diff --git a/BindingMap.h b/BindingMap.h index 7ad9218..a3b5331 100644 --- a/BindingMap.h +++ b/BindingMap.h @@ -232,12 +232,13 @@ struct EventKey * (CreatureEvents, GameObjectEvents, etc.). */ template -struct EntryKey : public EventKey +struct EntryKey { + T event_id; uint32 entry; - EntryKey(T event_type, uint32 entry) : - EventKey(event_type), + EntryKey(T event_id, uint32 entry) : + event_id(event_id), entry(entry) {} }; @@ -247,13 +248,14 @@ struct EntryKey : public EventKey * (currently just CreatureEvents). */ template -struct UniqueObjectKey : public EventKey +struct UniqueObjectKey { + T event_id; uint64 guid; uint32 instance_id; - UniqueObjectKey(T event_type, uint64 guid, uint32 instance_id) : - EventKey(event_type), + UniqueObjectKey(T event_id, uint64 guid, uint32 instance_id) : + event_id(event_id), guid(guid), instance_id(instance_id) {} diff --git a/ElunaInstanceAI.cpp b/ElunaInstanceAI.cpp new file mode 100644 index 0000000..aa0a746 --- /dev/null +++ b/ElunaInstanceAI.cpp @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2010 - 2015 Eluna Lua Engine + * This program is free software licensed under GPL version 3 + * Please see the included DOCS/LICENSE.md for more information + */ + +#include "ElunaInstanceAI.h" +#include "ElunaUtility.h" +#include "lmarshal.h" + + +void ElunaInstanceAI::Initialize() +{ + LOCK_ELUNA; + + ASSERT(!sEluna->HasInstanceData(instance)); + + // Create a new table for instance data. + lua_State* L = sEluna->L; + lua_newtable(L); + sEluna->CreateInstanceData(instance); + + sEluna->OnInitialize(this); +} + +void ElunaInstanceAI::Load(const char* data) +{ + LOCK_ELUNA; + + // If we get passed NULL (i.e. `Reload` was called) then use + // the last known save data (or maybe just an empty string). + if (!data) + { + data = lastSaveData.c_str(); + } + else // Otherwise, copy the new data into our buffer. + { + lastSaveData.assign(data); + } + + if (data[0] == '\0') + { + ASSERT(!sEluna->HasInstanceData(instance)); + + // Create a new table for instance data. + lua_State* L = sEluna->L; + lua_newtable(L); + sEluna->CreateInstanceData(instance); + + sEluna->OnLoad(this); + // Stack: (empty) + return; + } + + size_t decodedLength; + const unsigned char* decodedData = ElunaUtil::DecodeData(data, &decodedLength); + lua_State* L = sEluna->L; + + if (decodedData) + { + // Stack: (empty) + + lua_pushcfunction(L, mar_decode); + lua_pushlstring(L, (const char*)decodedData, decodedLength); + // Stack: mar_decode, decoded_data + + // Call `mar_decode` and check for success. + if (lua_pcall(L, 1, 1, 0) == 0) + { + // Stack: data + // Only use the data if it's a table. + if (lua_istable(L, -1)) + { + sEluna->CreateInstanceData(instance); + // Stack: (empty) + sEluna->OnLoad(this); + // WARNING! lastSaveData might be different after `OnLoad` if the Lua code saved data. + } + else + { + ELUNA_LOG_ERROR("Error while loading instance data: Expected data to be a table (type 5), got type %d instead", lua_type(L, -1)); + lua_pop(L, 1); + // Stack: (empty) + + Initialize(); + } + } + else + { + // Stack: error_message + ELUNA_LOG_ERROR("Error while parsing instance data with lua-marshal: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + // Stack: (empty) + + Initialize(); + } + + delete[] decodedData; + } + else + { + ELUNA_LOG_ERROR("Error while decoding instance data: Data is not valid base-64"); + Initialize(); + } +} + +const char* ElunaInstanceAI::Save() const +{ + LOCK_ELUNA; + lua_State* L = sEluna->L; + // Stack: (empty) + + /* + * Need to cheat because this method actually does modify this instance, + * even though it's declared as `const`. + * + * Declaring virtual methods as `const` is BAD! + * Don't dictate to children that their methods must be pure. + */ + ElunaInstanceAI* self = const_cast(this); + + lua_pushcfunction(L, mar_encode); + sEluna->PushInstanceData(L, self, false); + // Stack: mar_encode, instance_data + + if (lua_pcall(L, 1, 1, 0) != 0) + { + // Stack: error_message + ELUNA_LOG_ERROR("Error while saving: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + return NULL; + } + + // Stack: data + size_t dataLength; + const unsigned char* data = (const unsigned char*)lua_tolstring(L, -1, &dataLength); + ElunaUtil::EncodeData(data, dataLength, self->lastSaveData); + + lua_pop(L, 1); + // Stack: (empty) + + return lastSaveData.c_str(); +} + +uint32 ElunaInstanceAI::GetData(uint32 key) const +{ + LOCK_ELUNA; + lua_State* L = sEluna->L; + // Stack: (empty) + + sEluna->PushInstanceData(L, const_cast(this), false); + // Stack: instance_data + + Eluna::Push(L, key); + // Stack: instance_data, key + + lua_gettable(L, -2); + // Stack: instance_data, value + + uint32 value = Eluna::CHECKVAL(L, -1, 0); + lua_pop(L, 2); + // Stack: (empty) + + return value; +} + +void ElunaInstanceAI::SetData(uint32 key, uint32 value) +{ + LOCK_ELUNA; + lua_State* L = sEluna->L; + // Stack: (empty) + + sEluna->PushInstanceData(L, this, false); + // Stack: instance_data + + Eluna::Push(L, key); + Eluna::Push(L, value); + // Stack: instance_data, key, value + + lua_settable(L, -3); + // Stack: instance_data + + lua_pop(L, 1); + // Stack: (empty) +} + +uint64 ElunaInstanceAI::GetData64(uint32 key) const +{ + LOCK_ELUNA; + lua_State* L = sEluna->L; + // Stack: (empty) + + sEluna->PushInstanceData(L, const_cast(this), false); + // Stack: instance_data + + Eluna::Push(L, key); + // Stack: instance_data, key + + lua_gettable(L, -2); + // Stack: instance_data, value + + uint64 value = Eluna::CHECKVAL(L, -1, 0); + lua_pop(L, 2); + // Stack: (empty) + + return value; +} + +void ElunaInstanceAI::SetData64(uint32 key, uint64 value) +{ + LOCK_ELUNA; + lua_State* L = sEluna->L; + // Stack: (empty) + + sEluna->PushInstanceData(L, this, false); + // Stack: instance_data + + Eluna::Push(L, key); + Eluna::Push(L, value); + // Stack: instance_data, key, value + + lua_settable(L, -3); + // Stack: instance_data + + lua_pop(L, 1); + // Stack: (empty) +} diff --git a/ElunaInstanceAI.h b/ElunaInstanceAI.h new file mode 100644 index 0000000..68450d0 --- /dev/null +++ b/ElunaInstanceAI.h @@ -0,0 +1,151 @@ +/* +* Copyright (C) 2010 - 2015 Eluna Lua Engine +* This program is free software licensed under GPL version 3 +* Please see the included DOCS/LICENSE.md for more information +*/ + +#ifndef _ELUNA_INSTANCE_DATA_H +#define _ELUNA_INSTANCE_DATA_H + +#include "LuaEngine.h" +#ifdef TRINITY +#include "InstanceScript.h" +#else +#include "InstanceData.h" +#endif + + +/* + * This class is a small wrapper around `InstanceData`, + * allowing instances to be scripted with Eluna. + * + * + * Note 1 + * ====== + * + * Instances of `ElunaInstanceAI` are owned by the core, so they + * are not deleted when Eluna is reloaded. Thus `Load` is only called + * by the core once, no matter how many times Eluna is reloaded. + * + * However, when Eluna reloads, all instance data in Eluna is lost. + * So the solution is as follows: + * + * 1. Store the last save data in the member var `lastSaveData`. + * + * At first this is just the data given to us by the core when it calls `Load`, + * but later on once we start saving new data this is from Eluna. + * + * 2. When retrieving instance data from Eluna, check if it's missing. + * + * The data will be missing if Eluna is reloaded, since a new Lua state is created. + * + * 3. If it *is* missing, call `Reload`. + * + * This reloads the last known instance save data into Eluna, and calls the appropriate hooks. + * + * + * Note 2 + * ====== + * + * CMaNGOS expects some of these methods to be `const`. However, any of these + * methods are free to call `Save`, resulting in mutation of `lastSaveData`. + * + * Therefore, none of the hooks are `const`-safe, and `const_cast` is used + * to escape from these restrictions. + */ +class ElunaInstanceAI : public InstanceData +{ +private: + // The last save data to pass through this class, + // either through `Load` or `Save`. + std::string lastSaveData; + + // Used to prevent saving this AI while it's still loading, + // effectively nuking the data that's supposed to be loaded. + // Set to `false` while loading, `true` otherwise. + bool canSave; + +public: + ElunaInstanceAI(Map* map) : InstanceData(map), canSave(true) + { + } + + void Initialize() override; + + /* + * These are responsible for serializing/deserializing the instance's + * data table to/from the core. + */ + void Load(const char* data) override; +#ifdef TRINITY + // Simply calls Save, since the functions are a bit different in name and data types on different cores + std::string GetSaveData() override + { + return Save(); + } + const char* Save() const; +#else + const char* Save() const override; +#endif + + + /* + * Calls `Load` with the last save data that was passed to + * or from Eluna. + * + * See: big documentation blurb at the top of this class. + */ + void Reload() + { + Load(NULL); + } + + /* + * These methods allow non-Lua scripts (e.g. DB, C++) to get/set instance data. + */ + uint32 GetData(uint32 key) const override; + void SetData(uint32 key, uint32 value) override; + + uint64 GetData64(uint32 key) const override; + void SetData64(uint32 key, uint64 value) override; + + /* + * These methods are just thin wrappers around Eluna. + */ + void Update(uint32 diff) override + { + // If Eluna is reloaded, it will be missing our instance data. + // Reload here instead of waiting for the next hook call (possibly never). + // This avoids having to have an empty Update hook handler just to trigger the reload. + if (!sEluna->HasInstanceData(instance)) + Reload(); + + sEluna->OnUpdateInstance(this, diff); + } + + bool IsEncounterInProgress() const override + { + return sEluna->OnCheckEncounterInProgress(const_cast(this)); + } + + void OnPlayerEnter(Player* player) override + { + sEluna->OnPlayerEnterInstance(this, player); + } + +#ifdef TRINITY + void OnGameObjectCreate(GameObject* gameobject) override +#else + void OnObjectCreate(GameObject* gameobject) override +#endif + { + sEluna->OnGameObjectCreate(this, gameobject); + } + + void OnCreatureCreate(Creature* creature) override + { + sEluna->OnCreatureCreate(this, creature); + } +}; + +#endif // _ELUNA_INSTANCE_DATA_H \ No newline at end of file diff --git a/ElunaUtility.cpp b/ElunaUtility.cpp index c8efac9..06a653e 100644 --- a/ElunaUtility.cpp +++ b/ElunaUtility.cpp @@ -85,3 +85,97 @@ bool ElunaUtil::WorldObjectInRangeCheck::operator()(WorldObject* u) i_range = i_obj->GetDistance(u); return true; } + +static char encoding_table[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', '/'}; +static char decoding_table[256]; +static int mod_table[] = {0, 2, 1}; + +static void build_decoding_table() +{ + for (int i = 0; i < 64; i++) + decoding_table[(unsigned char)encoding_table[i]] = i; +} + +void ElunaUtil::EncodeData(const unsigned char* data, size_t input_length, std::string& output) +{ + size_t output_length = 4 * ((input_length + 2) / 3); + char* buffer = new char[output_length]; + + for (size_t i = 0, j = 0; i < input_length;) + { + uint32_t octet_a = i < input_length ? (unsigned char)data[i++] : 0; + uint32_t octet_b = i < input_length ? (unsigned char)data[i++] : 0; + uint32_t octet_c = i < input_length ? (unsigned char)data[i++] : 0; + + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + + buffer[j++] = encoding_table[(triple >> (3 * 6)) & 0x3F]; + buffer[j++] = encoding_table[(triple >> (2 * 6)) & 0x3F]; + buffer[j++] = encoding_table[(triple >> (1 * 6)) & 0x3F]; + buffer[j++] = encoding_table[(triple >> (0 * 6)) & 0x3F]; + } + + for (int i = 0; i < mod_table[input_length % 3]; i++) + buffer[output_length - 1 - i] = '='; + + output.assign(buffer, output_length); // Need length because `buffer` is not terminated! + delete[] buffer; +} + +unsigned char* ElunaUtil::DecodeData(const char *data, size_t *output_length) +{ + if (decoding_table['B'] == 0) + build_decoding_table(); + + size_t input_length = strlen(data); + + if (input_length % 4 != 0) + return NULL; + + // Make sure there's no invalid characters in the data. + for (size_t i = 0; i < input_length; ++i) + { + unsigned char byte = data[i]; + + if (byte == '=') + continue; + + // Every invalid character (and 'A') will map to 0 (due to `calloc`). + if (decoding_table[byte] == 0 && byte != 'A') + return NULL; + } + + *output_length = input_length / 4 * 3; + if (data[input_length - 1] == '=') (*output_length)--; + if (data[input_length - 2] == '=') (*output_length)--; + + unsigned char *decoded_data = new unsigned char[*output_length]; + if (!decoded_data) + return NULL; + + for (size_t i = 0, j = 0; i < input_length;) + { + uint32_t sextet_a = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]]; + uint32_t sextet_b = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]]; + uint32_t sextet_c = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]]; + uint32_t sextet_d = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]]; + + uint32_t triple = (sextet_a << (3 * 6)) + + (sextet_b << (2 * 6)) + + (sextet_c << (1 * 6)) + + (sextet_d << (0 * 6)); + + if (j < *output_length) decoded_data[j++] = (triple >> (2 * 8)) & 0xFF; + if (j < *output_length) decoded_data[j++] = (triple >> (1 * 8)) & 0xFF; + if (j < *output_length) decoded_data[j++] = (triple >> (0 * 8)) & 0xFF; + } + + return decoded_data; +} diff --git a/ElunaUtility.h b/ElunaUtility.h index f04e6ec..4cd2137 100644 --- a/ElunaUtility.h +++ b/ElunaUtility.h @@ -137,6 +137,18 @@ namespace ElunaUtil private: LockType _lock; }; + + /* + * Encodes `data` in Base-64 and store the result in `output`. + */ + void EncodeData(const unsigned char* data, size_t input_length, std::string& output); + + /* + * Decodes `data` from Base-64 and returns a pointer to the result, or `NULL` on error. + * + * The returned result buffer must be `delete[]`ed by the caller. + */ + unsigned char* DecodeData(const char* data, size_t *output_length); }; #endif diff --git a/GlobalMethods.h b/GlobalMethods.h index 8d68fd6..519343e 100644 --- a/GlobalMethods.h +++ b/GlobalMethods.h @@ -499,7 +499,7 @@ namespace LuaGlobalFunctions static int RegisterEntryHelper(Eluna* E, lua_State* L, int regtype) { - uint32 entry = Eluna::CHECKVAL(L, 1); + uint32 id = Eluna::CHECKVAL(L, 1); uint32 ev = Eluna::CHECKVAL(L, 2); luaL_checktype(L, 3, LUA_TFUNCTION); uint32 shots = Eluna::CHECKVAL(L, 4, 0); @@ -507,7 +507,7 @@ namespace LuaGlobalFunctions lua_pushvalue(L, 3); int functionRef = luaL_ref(L, LUA_REGISTRYINDEX); if (functionRef >= 0) - return E->Register(L, regtype, entry, 0, 0, ev, functionRef, shots); + return E->Register(L, regtype, id, 0, 0, ev, functionRef, shots); else luaL_argerror(L, 3, "unable to make a ref to function"); return 0; @@ -923,6 +923,60 @@ namespace LuaGlobalFunctions return RegisterEntryHelper(E, L, Hooks::REGTYPE_ITEM_GOSSIP); } + /** + * Registers a [Map] event handler for all instance of a [Map]. + * + *
+     * enum InstanceEvents
+     * {
+     *     INSTANCE_EVENT_ON_INITIALIZE                    = 1,    // (event, instance_data, map)
+     *     INSTANCE_EVENT_ON_LOAD                          = 2,    // (event, instance_data, map)
+     *     INSTANCE_EVENT_ON_UPDATE                        = 3,    // (event, instance_data, map, diff)
+     *     INSTANCE_EVENT_ON_PLAYER_ENTER                  = 4,    // (event, instance_data, map, player)
+     *     INSTANCE_EVENT_ON_CREATURE_CREATE               = 5,    // (event, instance_data, map, creature)
+     *     INSTANCE_EVENT_ON_GAMEOBJECT_CREATE             = 6,    // (event, instance_data, map, go)
+     *     INSTANCE_EVENT_ON_CHECK_ENCOUNTER_IN_PROGRESS   = 7,    // (event, instance_data, map)
+     *     INSTANCE_EVENT_COUNT
+     * };
+     * 
+ * + * @param uint32 map_id : ID of a [Map] + * @param uint32 event : [Map] event ID, refer to MapEvents above + * @param function function : function to register + * @param uint32 shots = 0 : the number of times the function will be called, 0 means "always call this function" + */ + int RegisterMapEvent(Eluna* E, lua_State* L) + { + return RegisterEntryHelper(E, L, Hooks::REGTYPE_MAP); + } + + /** + * Registers a [Map] event handler for one instance of a [Map]. + * + *
+     * enum InstanceEvents
+     * {
+     *     INSTANCE_EVENT_ON_INITIALIZE                    = 1,    // (event, instance_data, map)
+     *     INSTANCE_EVENT_ON_LOAD                          = 2,    // (event, instance_data, map)
+     *     INSTANCE_EVENT_ON_UPDATE                        = 3,    // (event, instance_data, map, diff)
+     *     INSTANCE_EVENT_ON_PLAYER_ENTER                  = 4,    // (event, instance_data, map, player)
+     *     INSTANCE_EVENT_ON_CREATURE_CREATE               = 5,    // (event, instance_data, map, creature)
+     *     INSTANCE_EVENT_ON_GAMEOBJECT_CREATE             = 6,    // (event, instance_data, map, go)
+     *     INSTANCE_EVENT_ON_CHECK_ENCOUNTER_IN_PROGRESS   = 7,    // (event, instance_data, map)
+     *     INSTANCE_EVENT_COUNT
+     * };
+     * 
+ * + * @param uint32 instance_id : ID of an instance of a [Map] + * @param uint32 event : [Map] event ID, refer to MapEvents above + * @param function function : function to register + * @param uint32 shots = 0 : the number of times the function will be called, 0 means "always call this function" + */ + int RegisterInstanceEvent(Eluna* E, lua_State* L) + { + return RegisterEntryHelper(E, L, Hooks::REGTYPE_INSTANCE); + } + /** * Registers a [Player] gossip event handler. * @@ -2898,5 +2952,71 @@ namespace LuaGlobalFunctions } return 0; } + + /** + * Unbinds event handlers for either all of a non-instanced [Map]'s events, or one type of event. + * + * If `event_type` is `nil`, all the non-instanced [Map]'s event handlers are cleared. + * + * Otherwise, only event handlers for `event_type` are cleared. + * + * @proto (map_id) + * @proto (map_id, event_type) + * @param uint32 map_id : the ID of a [Map] + * @param uint32 event_type : the event whose handlers will be cleared, see [Global:RegisterPlayerGossipEvent] + */ + int ClearMapEvents(Eluna* E, lua_State* L) + { + typedef EntryKey Key; + + if (lua_isnoneornil(L, 2)) + { + uint32 entry = Eluna::CHECKVAL(L, 1); + + for (uint32 i = 1; i < Hooks::INSTANCE_EVENT_COUNT; ++i) + E->MapEventBindings->Clear(Key((Hooks::InstanceEvents)i, entry)); + } + else + { + uint32 entry = Eluna::CHECKVAL(L, 1); + uint32 event_type = Eluna::CHECKVAL(L, 2); + E->MapEventBindings->Clear(Key((Hooks::InstanceEvents)event_type, entry)); + } + + return 0; + } + + /** + * Unbinds event handlers for either all of an instanced [Map]'s events, or one type of event. + * + * If `event_type` is `nil`, all the instanced [Map]'s event handlers are cleared. + * + * Otherwise, only event handlers for `event_type` are cleared. + * + * @proto (instance_id) + * @proto (instance_id, event_type) + * @param uint32 entry : the ID of an instance of a [Map] + * @param uint32 event_type : the event whose handlers will be cleared, see [Global:RegisterPlayerGossipEvent] + */ + int ClearInstanceEvents(Eluna* E, lua_State* L) + { + typedef EntryKey Key; + + if (lua_isnoneornil(L, 2)) + { + uint32 entry = Eluna::CHECKVAL(L, 1); + + for (uint32 i = 1; i < Hooks::INSTANCE_EVENT_COUNT; ++i) + E->InstanceEventBindings->Clear(Key((Hooks::InstanceEvents)i, entry)); + } + else + { + uint32 entry = Eluna::CHECKVAL(L, 1); + uint32 event_type = Eluna::CHECKVAL(L, 2); + E->InstanceEventBindings->Clear(Key((Hooks::InstanceEvents)event_type, entry)); + } + + return 0; + } } #endif diff --git a/Hooks.h b/Hooks.h index 98ec86b..2d28351 100644 --- a/Hooks.h +++ b/Hooks.h @@ -84,6 +84,8 @@ namespace Hooks REGTYPE_ITEM_GOSSIP, REGTYPE_PLAYER_GOSSIP, REGTYPE_BG, + REGTYPE_MAP, + REGTYPE_INSTANCE, REGTYPE_COUNT }; @@ -332,6 +334,18 @@ namespace Hooks BG_EVENT_ON_PRE_DESTROY = 4, // (event, bg, bgId, instanceId) - Needs to be added to TC BG_EVENT_COUNT }; + + enum InstanceEvents + { + INSTANCE_EVENT_ON_INITIALIZE = 1, // (event, instance_data, map) + INSTANCE_EVENT_ON_LOAD = 2, // (event, instance_data, map) + INSTANCE_EVENT_ON_UPDATE = 3, // (event, instance_data, map, diff) + INSTANCE_EVENT_ON_PLAYER_ENTER = 4, // (event, instance_data, map, player) + INSTANCE_EVENT_ON_CREATURE_CREATE = 5, // (event, instance_data, map, creature) + INSTANCE_EVENT_ON_GAMEOBJECT_CREATE = 6, // (event, instance_data, map, go) + INSTANCE_EVENT_ON_CHECK_ENCOUNTER_IN_PROGRESS = 7, // (event, instance_data, map) + INSTANCE_EVENT_COUNT + }; }; #endif // _HOOKS_H diff --git a/InstanceHooks.cpp b/InstanceHooks.cpp new file mode 100644 index 0000000..9646dcc --- /dev/null +++ b/InstanceHooks.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2010 - 2015 Eluna Lua Engine + * This program is free software licensed under GPL version 3 + * Please see the included DOCS/LICENSE.md for more information + */ + +#include "Hooks.h" +#include "HookHelpers.h" +#include "LuaEngine.h" +#include "BindingMap.h" +#include "ElunaIncludes.h" +#include "ElunaTemplate.h" +#include "ElunaInstanceAI.h" + +using namespace Hooks; + +#define START_HOOK(EVENT, AI) \ + if (!IsEnabled())\ + return;\ + auto mapKey = EntryKey(EVENT, AI->instance->GetId());\ + auto instanceKey = EntryKey(EVENT, AI->instance->GetInstanceId());\ + if (!MapEventBindings->HasBindingsFor(mapKey) && !InstanceEventBindings->HasBindingsFor(instanceKey))\ + return;\ + LOCK_ELUNA;\ + PushInstanceData(L, AI);\ + Push(AI->instance) + +#define START_HOOK_WITH_RETVAL(EVENT, AI, RETVAL) \ + if (!IsEnabled())\ + return RETVAL;\ + auto mapKey = EntryKey(EVENT, AI->instance->GetId());\ + auto instanceKey = EntryKey(EVENT, AI->instance->GetInstanceId());\ + if (!MapEventBindings->HasBindingsFor(mapKey) && !InstanceEventBindings->HasBindingsFor(instanceKey))\ + return RETVAL;\ + LOCK_ELUNA;\ + PushInstanceData(L, AI);\ + Push(AI->instance) + +void Eluna::OnInitialize(ElunaInstanceAI* ai) +{ + START_HOOK(INSTANCE_EVENT_ON_INITIALIZE, ai); + CallAllFunctions(MapEventBindings, InstanceEventBindings, mapKey, instanceKey); +} + +void Eluna::OnLoad(ElunaInstanceAI* ai) +{ + START_HOOK(INSTANCE_EVENT_ON_LOAD, ai); + CallAllFunctions(MapEventBindings, InstanceEventBindings, mapKey, instanceKey); +} + +void Eluna::OnUpdateInstance(ElunaInstanceAI* ai, uint32 diff) +{ + START_HOOK(INSTANCE_EVENT_ON_UPDATE, ai); + Push(diff); + CallAllFunctions(MapEventBindings, InstanceEventBindings, mapKey, instanceKey); +} + +void Eluna::OnPlayerEnterInstance(ElunaInstanceAI* ai, Player* player) +{ + START_HOOK(INSTANCE_EVENT_ON_PLAYER_ENTER, ai); + Push(player); + CallAllFunctions(MapEventBindings, InstanceEventBindings, mapKey, instanceKey); +} + +void Eluna::OnCreatureCreate(ElunaInstanceAI* ai, Creature* creature) +{ + START_HOOK(INSTANCE_EVENT_ON_CREATURE_CREATE, ai); + Push(creature); + CallAllFunctions(MapEventBindings, InstanceEventBindings, mapKey, instanceKey); +} + +void Eluna::OnGameObjectCreate(ElunaInstanceAI* ai, GameObject* gameobject) +{ + START_HOOK(INSTANCE_EVENT_ON_GAMEOBJECT_CREATE, ai); + Push(gameobject); + CallAllFunctions(MapEventBindings, InstanceEventBindings, mapKey, instanceKey); +} + +bool Eluna::OnCheckEncounterInProgress(ElunaInstanceAI* ai) +{ + START_HOOK_WITH_RETVAL(INSTANCE_EVENT_ON_CHECK_ENCOUNTER_IN_PROGRESS, ai, false); + return CallAllFunctionsBool(MapEventBindings, InstanceEventBindings, mapKey, instanceKey); +} diff --git a/LuaEngine.cpp b/LuaEngine.cpp index 7905abe..38e38f5 100644 --- a/LuaEngine.cpp +++ b/LuaEngine.cpp @@ -12,6 +12,7 @@ #include "ElunaTemplate.h" #include "ElunaUtility.h" #include "ElunaCreatureAI.h" +#include "ElunaInstanceAI.h" #ifdef USING_BOOST #include @@ -47,6 +48,12 @@ void Eluna::Initialize() LOCK_ELUNA; ASSERT(!IsInitialized()); +#ifdef TRINITY + // For instance data the data column needs to be able to hold more than 255 characters (tinytext) + // so we change it to TEXT automatically on startup + CharacterDatabase.DirectExecute("ALTER TABLE `instance` CHANGE COLUMN `data` `data` TEXT NOT NULL"); +#endif + LoadScriptPaths(); // Must be before creating GEluna @@ -152,6 +159,8 @@ GameObjectGossipBindings(NULL), ItemEventBindings(NULL), ItemGossipBindings(NULL), PlayerGossipBindings(NULL), +MapEventBindings(NULL), +InstanceEventBindings(NULL), CreatureUniqueBindings(NULL) { @@ -185,6 +194,9 @@ void Eluna::CloseLua() if (L) lua_close(L); L = NULL; + + instanceDataRefs.clear(); + continentDataRefs.clear(); } void Eluna::OpenLua() @@ -243,6 +255,8 @@ void Eluna::CreateBindStores() ItemEventBindings = new BindingMap< EntryKey >(L); ItemGossipBindings = new BindingMap< EntryKey >(L); PlayerGossipBindings = new BindingMap< EntryKey >(L); + MapEventBindings = new BindingMap< EntryKey >(L); + InstanceEventBindings = new BindingMap< EntryKey >(L); CreatureUniqueBindings = new BindingMap< UniqueObjectKey >(L); } @@ -264,6 +278,8 @@ void Eluna::DestroyBindStores() delete ItemGossipBindings; delete PlayerGossipBindings; delete BGEventBindings; + delete MapEventBindings; + delete InstanceEventBindings; delete CreatureUniqueBindings; @@ -282,6 +298,8 @@ void Eluna::DestroyBindStores() ItemGossipBindings = NULL; PlayerGossipBindings = NULL; BGEventBindings = NULL; + MapEventBindings = NULL; + InstanceEventBindings = NULL; CreatureUniqueBindings = NULL; } @@ -1136,6 +1154,24 @@ int Eluna::Register(lua_State* L, uint8 regtype, uint32 entry, uint64 guid, uint return 1; // Stack: callback } break; + case Hooks::REGTYPE_MAP: + if (event_id < Hooks::INSTANCE_EVENT_COUNT) + { + auto key = EntryKey((Hooks::InstanceEvents)event_id, entry); + bindingID = MapEventBindings->Insert(key, functionRef, shots); + createCancelCallback(L, bindingID, MapEventBindings); + return 1; // Stack: callback + } + break; + case Hooks::REGTYPE_INSTANCE: + if (event_id < Hooks::INSTANCE_EVENT_COUNT) + { + auto key = EntryKey((Hooks::InstanceEvents)event_id, entry); + bindingID = InstanceEventBindings->Insert(key, functionRef, shots); + createCancelCallback(L, bindingID, InstanceEventBindings); + return 1; // Stack: callback + } + break; } luaL_unref(L, LUA_REGISTRYINDEX, functionRef); luaL_error(L, "Unknown event type (regtype %hhu, event %u, entry %u, guid " UI64FMTD ", instance %u)", regtype, event_id, entry, guid, instanceId); @@ -1205,3 +1241,107 @@ CreatureAI* Eluna::GetAI(Creature* creature) return NULL; } + +InstanceData* Eluna::GetInstanceData(Map* map) +{ + if (!IsEnabled()) + return NULL; + + for (int i = 1; i < Hooks::INSTANCE_EVENT_COUNT; ++i) + { + Hooks::InstanceEvents event_id = (Hooks::InstanceEvents)i; + + auto key = EntryKey(event_id, map->GetId()); + + if (MapEventBindings->HasBindingsFor(key) || + InstanceEventBindings->HasBindingsFor(key)) + return new ElunaInstanceAI(map); + } + + return NULL; +} + +bool Eluna::HasInstanceData(Map const* map) +{ + if (!map->Instanceable()) + return continentDataRefs.find(map->GetId()) != continentDataRefs.end(); + else + return instanceDataRefs.find(map->GetInstanceId()) != instanceDataRefs.end(); +} + +void Eluna::CreateInstanceData(Map const* map) +{ + ASSERT(lua_istable(L, -1)); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); + + if (!map->Instanceable()) + { + uint32 mapId = map->GetId(); + + // If there's another table that was already stored for the map, unref it. + auto mapRef = continentDataRefs.find(mapId); + if (mapRef != continentDataRefs.end()) + { + luaL_unref(L, LUA_REGISTRYINDEX, mapRef->second); + } + + continentDataRefs[mapId] = ref; + } + else + { + uint32 instanceId = map->GetInstanceId(); + + // If there's another table that was already stored for the instance, unref it. + auto instRef = instanceDataRefs.find(instanceId); + if (instRef != instanceDataRefs.end()) + { + luaL_unref(L, LUA_REGISTRYINDEX, instRef->second); + } + + instanceDataRefs[instanceId] = ref; + } +} + +/* + * Unrefs the instanceId related events and data + * Does all required actions for when an instance is freed. + */ +void Eluna::FreeInstanceId(uint32 instanceId) +{ + LOCK_ELUNA; + + for (int i = 1; i < Hooks::INSTANCE_EVENT_COUNT; ++i) + { + auto key = EntryKey((Hooks::InstanceEvents)i, instanceId); + + if (MapEventBindings->HasBindingsFor(key)) + MapEventBindings->Clear(key); + + if (InstanceEventBindings->HasBindingsFor(key)) + InstanceEventBindings->Clear(key); + + if (instanceDataRefs.find(instanceId) != instanceDataRefs.end()) + { + luaL_unref(L, LUA_REGISTRYINDEX, instanceDataRefs[instanceId]); + instanceDataRefs.erase(instanceId); + } + } +} + +void Eluna::PushInstanceData(lua_State* L, ElunaInstanceAI* ai, bool incrementCounter) +{ + // Check if the instance data is missing (i.e. someone reloaded Eluna). + if (!HasInstanceData(ai->instance)) + ai->Reload(); + + // Get the instance data table from the registry. + if (!ai->instance->Instanceable()) + lua_rawgeti(L, LUA_REGISTRYINDEX, continentDataRefs[ai->instance->GetId()]); + else + lua_rawgeti(L, LUA_REGISTRYINDEX, instanceDataRefs[ai->instance->GetInstanceId()]); + + ASSERT(lua_istable(L, -1)); + + if (incrementCounter) + ++push_counter; +} diff --git a/LuaEngine.h b/LuaEngine.h index 2da30c5..73144af 100644 --- a/LuaEngine.h +++ b/LuaEngine.h @@ -19,6 +19,7 @@ #include "Weather.h" #include "World.h" #include "Hooks.h" +#include "ElunaUtility.h" extern "C" { @@ -56,6 +57,13 @@ class GameObjectAI; #endif class Guild; class Group; +#ifdef TRINITY +class InstanceScript; +typedef InstanceScript InstanceData; +#else +class InstanceData; +#endif +class ElunaInstanceAI; class Item; class Pet; class Player; @@ -136,6 +144,11 @@ private: uint8 push_counter; bool enabled; + // Map from instance ID -> Lua table ref + std::unordered_map instanceDataRefs; + // Map from map ID -> Lua table ref + std::unordered_map continentDataRefs; + Eluna(); ~Eluna(); @@ -184,6 +197,23 @@ private: return CallAllFunctionsBool(bindings, NULL, key, key, default_value); } + // Non-static pushes, to be used in hooks. + // These just call the correct static version with the main thread's Lua state. + void Push() { Push(L); ++push_counter; } + void Push(const long long value) { Push(L, value); ++push_counter; } + void Push(const unsigned long long value) { Push(L, value); ++push_counter; } + void Push(const long value) { Push(L, value); ++push_counter; } + void Push(const unsigned long value) { Push(L, value); ++push_counter; } + void Push(const int value) { Push(L, value); ++push_counter; } + void Push(const unsigned int value) { Push(L, value); ++push_counter; } + void Push(const bool value) { Push(L, value); ++push_counter; } + void Push(const float value) { Push(L, value); ++push_counter; } + void Push(const double value) { Push(L, value); ++push_counter; } + void Push(const std::string& value) { Push(L, value); ++push_counter; } + void Push(const char* value) { Push(L, value); ++push_counter; } + template + void Push(T const* ptr) { Push(L, ptr); ++push_counter; } + public: static Eluna* GEluna; @@ -205,6 +235,8 @@ public: BindingMap< EntryKey >* ItemEventBindings; BindingMap< EntryKey >* ItemGossipBindings; BindingMap< EntryKey >* PlayerGossipBindings; + BindingMap< EntryKey >* MapEventBindings; + BindingMap< EntryKey >* InstanceEventBindings; BindingMap< UniqueObjectKey >* CreatureUniqueBindings; @@ -239,29 +271,36 @@ public: ElunaTemplate::Push(luastate, ptr); } + /* + * Returns `true` if Eluna has instance data for `map`. + */ + bool HasInstanceData(Map const* map); + + /* + * Use the top element of the stack as the instance data table for `map`, + * then pops it off the stack. + */ + void CreateInstanceData(Map const* map); + + /* + * Retrieve the instance data for the `Map` scripted by `ai` and push it + * onto the stack. + * + * An `ElunaInstanceAI` is needed because the instance data might + * not exist (i.e. Eluna has been reloaded). + * + * In that case, the AI is "reloaded" (new instance data table is created + * and loaded with the last known save state, and `Load`/`Initialize` + * hooks are called). + */ + void PushInstanceData(lua_State* L, ElunaInstanceAI* ai, bool incrementCounter = true); + void RunScripts(); bool ShouldReload() const { return reload; } bool IsEnabled() const { return enabled && IsInitialized(); } bool HasLuaState() const { return L != NULL; } int Register(lua_State* L, uint8 reg, uint32 entry, uint64 guid, uint32 instanceId, uint32 event_id, int functionRef, uint32 shots); - // Non-static pushes, to be used in hooks. - // These just call the correct static version with the main thread's Lua state. - void Push() { Push(L); ++push_counter; } - void Push(const long long value) { Push(L, value); ++push_counter; } - void Push(const unsigned long long value) { Push(L, value); ++push_counter; } - void Push(const long value) { Push(L, value); ++push_counter; } - void Push(const unsigned long value) { Push(L, value); ++push_counter; } - void Push(const int value) { Push(L, value); ++push_counter; } - void Push(const unsigned int value) { Push(L, value); ++push_counter; } - void Push(const bool value) { Push(L, value); ++push_counter; } - void Push(const float value) { Push(L, value); ++push_counter; } - void Push(const double value) { Push(L, value); ++push_counter; } - void Push(const std::string& value) { Push(L, value); ++push_counter; } - void Push(const char* value) { Push(L, value); ++push_counter; } - template - void Push(T const* ptr) { Push(L, ptr); ++push_counter; } - // Checks template static T CHECKVAL(lua_State* luastate, int narg); template static T CHECKVAL(lua_State* luastate, int narg, T def) @@ -275,6 +314,8 @@ public: static ElunaObject* CHECKTYPE(lua_State* luastate, int narg, const char *tname, bool error = true); CreatureAI* GetAI(Creature* creature); + InstanceData* GetInstanceData(Map* map); + void FreeInstanceId(uint32 instanceId); /* Custom */ void OnTimedEvent(int funcRef, uint32 delay, uint32 calls, WorldObject* obj); @@ -454,6 +495,15 @@ public: void OnRemove(Creature* creature); void OnRemove(GameObject* gameobject); + /* Instance */ + void OnInitialize(ElunaInstanceAI* ai); + void OnLoad(ElunaInstanceAI* ai); + void OnUpdateInstance(ElunaInstanceAI* ai, uint32 diff); + void OnPlayerEnterInstance(ElunaInstanceAI* ai, Player* player); + void OnCreatureCreate(ElunaInstanceAI* ai, Creature* creature); + void OnGameObjectCreate(ElunaInstanceAI* ai, GameObject* gameobject); + bool OnCheckEncounterInProgress(ElunaInstanceAI* ai); + /* World */ void OnOpenStateChange(bool open); void OnConfigLoad(bool reload); diff --git a/LuaFunctions.cpp b/LuaFunctions.cpp index 34be66e..cd3f5d8 100644 --- a/LuaFunctions.cpp +++ b/LuaFunctions.cpp @@ -54,6 +54,8 @@ ElunaGlobal::ElunaRegister GlobalMethods[] = { "RegisterItemGossipEvent", &LuaGlobalFunctions::RegisterItemGossipEvent }, // RegisterItemGossipEvent(entry, event, function) { "RegisterPlayerGossipEvent", &LuaGlobalFunctions::RegisterPlayerGossipEvent }, // RegisterPlayerGossipEvent(menu_id, event, function) { "RegisterBGEvent", &LuaGlobalFunctions::RegisterBGEvent }, // RegisterBGEvent(event, function) + { "RegisterMapEvent", &LuaGlobalFunctions::RegisterMapEvent }, + { "RegisterInstanceEvent", &LuaGlobalFunctions::RegisterInstanceEvent }, { "ClearBattleGroundEvents", &LuaGlobalFunctions::ClearBattleGroundEvents }, { "ClearCreatureEvents", &LuaGlobalFunctions::ClearCreatureEvents }, @@ -69,6 +71,8 @@ ElunaGlobal::ElunaRegister GlobalMethods[] = { "ClearPlayerEvents", &LuaGlobalFunctions::ClearPlayerEvents }, { "ClearPlayerGossipEvents", &LuaGlobalFunctions::ClearPlayerGossipEvents }, { "ClearServerEvents", &LuaGlobalFunctions::ClearServerEvents }, + { "ClearMapEvents", &LuaGlobalFunctions::ClearMapEvents }, + { "ClearInstanceEvents", &LuaGlobalFunctions::ClearInstanceEvents }, // Getters { "GetLuaEngine", &LuaGlobalFunctions::GetLuaEngine }, @@ -1190,6 +1194,7 @@ ElunaRegister MapMethods[] = { "GetName", &LuaMap::GetName }, // :GetName() - Returns the map's name UNDOCUMENTED { "GetDifficulty", &LuaMap::GetDifficulty }, // :GetDifficulty() - Returns the map's difficulty UNDOCUMENTED { "GetInstanceId", &LuaMap::GetInstanceId }, // :GetInstanceId() - Returns the map's instance ID UNDOCUMENTED + { "GetInstanceData", &LuaMap::GetInstanceData }, { "GetPlayerCount", &LuaMap::GetPlayerCount }, // :GetPlayerCount() - Returns the amount of players on map except GM's UNDOCUMENTED { "GetMapId", &LuaMap::GetMapId }, // :GetMapId() - Returns the map's ID UNDOCUMENTED { "GetAreaId", &LuaMap::GetAreaId }, // :GetAreaId(x, y, z) - Returns the map's area ID based on coords UNDOCUMENTED @@ -1211,6 +1216,9 @@ ElunaRegister MapMethods[] = #endif { "IsRaid", &LuaMap::IsRaid }, // :IsRaid() - Returns the true if the map is a raid map, else false UNDOCUMENTED + // Other + { "SaveInstanceData", &LuaMap::SaveInstanceData }, + { NULL, NULL }, }; diff --git a/MapMethods.h b/MapMethods.h index e4d2ab1..a39a420 100644 --- a/MapMethods.h +++ b/MapMethods.h @@ -7,6 +7,8 @@ #ifndef MAPMETHODS_H #define MAPMETHODS_H +#include "ElunaInstanceAI.h" + /*** * A game map, e.g. Azeroth, Eastern Kingdoms, the Molten Core, etc. * @@ -273,5 +275,50 @@ namespace LuaMap #endif return 0; } + + /** + * Gets the instance data table for the [Map], if it exists. + * + * The instance must be scripted using Eluna for this to succeed. + * If the instance is scripted in C++ this will return `nil`. + * + * @return table instance_data : instance data table, or `nil` + */ + int GetInstanceData(Eluna* E, lua_State* L, Map* map) + { +#ifdef TRINITY + ElunaInstanceAI* iAI = NULL; + if (InstanceMap* inst = map->ToInstanceMap()) + iAI = dynamic_cast(inst->GetInstanceScript()); +#else + ElunaInstanceAI* iAI = dynamic_cast(map->GetInstanceData()); +#endif + + if (iAI) + sEluna->PushInstanceData(L, iAI, false); + else + Eluna::Push(L); // nil + + return 1; + } + + /** + * Saves the [Map]'s instance data to the database. + */ + int SaveInstanceData(Eluna* E, lua_State* L, Map* map) + { +#ifdef TRINITY + ElunaInstanceAI* iAI = NULL; + if (InstanceMap* inst = map->ToInstanceMap()) + iAI = dynamic_cast(inst->GetInstanceScript()); +#else + ElunaInstanceAI* iAI = dynamic_cast(map->GetInstanceData()); +#endif + + if (iAI) + iAI->SaveToDB(); + + return 0; + } }; #endif diff --git a/lmarshal.cpp b/lmarshal.cpp new file mode 100644 index 0000000..a70ebde --- /dev/null +++ b/lmarshal.cpp @@ -0,0 +1,579 @@ +/* + * lmarshal.c + * A Lua library for serializing and deserializing Lua values + * Richard Hundt , Eluna Lua Engine + * + * License: MIT + * + * Copyright (c) 2010 Richard Hundt + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + +extern "C" { +#include "lua.h" +#include "lualib.h" +#include "lauxlib.h" +} + +#define MAR_TREF 1 +#define MAR_TVAL 2 +#define MAR_TUSR 3 + +#define MAR_CHR 1 +#define MAR_I32 4 +#define MAR_I64 8 + +#define MAR_MAGIC 0x8f +#define SEEN_IDX 3 + +#define MAR_ENV_IDX_KEY "E" +#define MAR_NUPS_IDX_KEY "n" + +typedef struct mar_Buffer { + size_t size; + size_t seek; + size_t head; + char* data; +} mar_Buffer; + +static int mar_encode_table(lua_State *L, mar_Buffer *buf, size_t *idx); +static int mar_decode_table(lua_State *L, const char* buf, size_t len, size_t *idx); + +static void buf_init(lua_State *L, mar_Buffer *buf) +{ + buf->size = 128; + buf->seek = 0; + buf->head = 0; + if (!(buf->data = (char*)malloc(buf->size))) luaL_error(L, "Out of memory!"); +} + +static void buf_done(lua_State* L, mar_Buffer *buf) +{ + free(buf->data); +} + +static int buf_write(lua_State* L, const char* str, size_t len, mar_Buffer *buf) +{ + if (len > UINT32_MAX) luaL_error(L, "buffer too long"); + if (buf->size - buf->head < len) { + size_t new_size = buf->size << 1; + size_t cur_head = buf->head; + while (new_size - cur_head <= len) { + new_size = new_size << 1; + } + if (!(buf->data = (char*)realloc(buf->data, new_size))) { + luaL_error(L, "Out of memory!"); + } + buf->size = new_size; + } + memcpy(&buf->data[buf->head], str, len); + buf->head += len; + return 0; +} + +static const char* buf_read(lua_State *L, mar_Buffer *buf, size_t *len) +{ + if (buf->seek < buf->head) { + buf->seek = buf->head; + *len = buf->seek; + return buf->data; + } + *len = 0; + return NULL; +} + +static void mar_encode_value(lua_State *L, mar_Buffer *buf, int val, size_t *idx) +{ + size_t l; + int val_type = lua_type(L, val); + lua_pushvalue(L, val); + + buf_write(L, (const char*)&val_type, MAR_CHR, buf); + switch (val_type) { + case LUA_TBOOLEAN: { + int int_val = lua_toboolean(L, -1); + buf_write(L, (const char*)&int_val, MAR_CHR, buf); + break; + } + case LUA_TSTRING: { + const char *str_val = lua_tolstring(L, -1, &l); + buf_write(L, (const char*)&l, MAR_I32, buf); + buf_write(L, str_val, l, buf); + break; + } + case LUA_TNUMBER: { + lua_Number num_val = lua_tonumber(L, -1); + buf_write(L, (const char*)&num_val, MAR_I64, buf); + break; + } + case LUA_TTABLE: { + int tag, ref; + lua_pushvalue(L, -1); + lua_rawget(L, SEEN_IDX); + if (!lua_isnil(L, -1)) { + ref = lua_tointeger(L, -1); + tag = MAR_TREF; + buf_write(L, (const char*)&tag, MAR_CHR, buf); + buf_write(L, (const char*)&ref, MAR_I32, buf); + lua_pop(L, 1); + } + else { + mar_Buffer rec_buf; + lua_pop(L, 1); /* pop nil */ + if (luaL_getmetafield(L, -1, "__persist")) { + tag = MAR_TUSR; + + lua_pushvalue(L, -2); /* self */ + lua_call(L, 1, 1); + if (!lua_isfunction(L, -1)) { + luaL_error(L, "__persist must return a function"); + } + + lua_remove(L, -2); /* __persist */ + + lua_newtable(L); + lua_pushvalue(L, -2); /* callback */ + lua_rawseti(L, -2, 1); + + buf_init(L, &rec_buf); + mar_encode_table(L, &rec_buf, idx); + + buf_write(L, (const char*)&tag, MAR_CHR, buf); + buf_write(L, (const char*)&rec_buf.head, MAR_I32, buf); + buf_write(L, rec_buf.data, rec_buf.head, buf); + buf_done(L, &rec_buf); + lua_pop(L, 1); + } + else { + tag = MAR_TVAL; + + lua_pushvalue(L, -1); + lua_pushinteger(L, (*idx)++); + lua_rawset(L, SEEN_IDX); + + lua_pushvalue(L, -1); + buf_init(L, &rec_buf); + mar_encode_table(L, &rec_buf, idx); + lua_pop(L, 1); + + buf_write(L, (const char*)&tag, MAR_CHR, buf); + buf_write(L, (const char*)&rec_buf.head, MAR_I32, buf); + buf_write(L, rec_buf.data,rec_buf.head, buf); + buf_done(L, &rec_buf); + } + } + break; + } + case LUA_TFUNCTION: { + int tag, ref; + lua_pushvalue(L, -1); + lua_rawget(L, SEEN_IDX); + if (!lua_isnil(L, -1)) { + ref = lua_tointeger(L, -1); + tag = MAR_TREF; + buf_write(L, (const char*)&tag, MAR_CHR, buf); + buf_write(L, (const char*)&ref, MAR_I32, buf); + lua_pop(L, 1); + } + else { + mar_Buffer rec_buf; + unsigned int i; + lua_Debug ar; + lua_pop(L, 1); /* pop nil */ + + lua_pushvalue(L, -1); + lua_getinfo(L, ">nuS", &ar); + if (ar.what[0] != 'L') { + luaL_error(L, "attempt to persist a C function '%s'", ar.name); + } + tag = MAR_TVAL; + lua_pushvalue(L, -1); + lua_pushinteger(L, (*idx)++); + lua_rawset(L, SEEN_IDX); + + lua_pushvalue(L, -1); + buf_init(L, &rec_buf); + lua_dump(L, (lua_Writer)buf_write, &rec_buf); + + buf_write(L, (const char*)&tag, MAR_CHR, buf); + buf_write(L, (const char*)&rec_buf.head, MAR_I32, buf); + buf_write(L, rec_buf.data, rec_buf.head, buf); + buf_done(L, &rec_buf); + lua_pop(L, 1); + + lua_newtable(L); + for (i = 1; i <= ar.nups; i++) { + const char* upvalue_name = lua_getupvalue(L, -2, i); + if (strcmp("_ENV", upvalue_name) == 0) { + lua_pop(L, 1); + // Mark where _ENV is expected. + lua_pushstring(L, MAR_ENV_IDX_KEY); + lua_pushinteger(L, i); + lua_rawset(L, -3); + } + else { + lua_rawseti(L, -2, i); + } + } + lua_pushstring(L, MAR_NUPS_IDX_KEY); + lua_pushnumber(L, ar.nups); + lua_rawset(L, -3); + + buf_init(L, &rec_buf); + mar_encode_table(L, &rec_buf, idx); + + buf_write(L, (const char*)&rec_buf.head, MAR_I32, buf); + buf_write(L, rec_buf.data, rec_buf.head, buf); + buf_done(L, &rec_buf); + lua_pop(L, 1); + } + + break; + } + case LUA_TUSERDATA: { + int tag, ref; + lua_pushvalue(L, -1); + lua_rawget(L, SEEN_IDX); + if (!lua_isnil(L, -1)) { + ref = lua_tointeger(L, -1); + tag = MAR_TREF; + buf_write(L, (const char*)&tag, MAR_CHR, buf); + buf_write(L, (const char*)&ref, MAR_I32, buf); + lua_pop(L, 1); + } + else { + mar_Buffer rec_buf; + lua_pop(L, 1); /* pop nil */ + if (luaL_getmetafield(L, -1, "__persist")) { + tag = MAR_TUSR; + + lua_pushvalue(L, -2); + lua_pushinteger(L, (*idx)++); + lua_rawset(L, SEEN_IDX); + + lua_pushvalue(L, -2); + lua_call(L, 1, 1); + if (!lua_isfunction(L, -1)) { + luaL_error(L, "__persist must return a function"); + } + lua_newtable(L); + lua_pushvalue(L, -2); + lua_rawseti(L, -2, 1); + lua_remove(L, -2); + + buf_init(L, &rec_buf); + mar_encode_table(L, &rec_buf, idx); + + buf_write(L, (const char*)&tag, MAR_CHR, buf); + buf_write(L, (const char*)&rec_buf.head, MAR_I32, buf); + buf_write(L, rec_buf.data, rec_buf.head, buf); + buf_done(L, &rec_buf); + } + else { + luaL_error(L, "attempt to encode userdata (no __persist hook)"); + } + lua_pop(L, 1); + } + break; + } + case LUA_TNIL: break; + default: + luaL_error(L, "invalid value type (%s)", lua_typename(L, val_type)); + } + lua_pop(L, 1); +} + +static int mar_encode_table(lua_State *L, mar_Buffer *buf, size_t *idx) +{ + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + mar_encode_value(L, buf, -2, idx); + mar_encode_value(L, buf, -1, idx); + lua_pop(L, 1); + } + return 1; +} + +#define mar_incr_ptr(l) \ + if (((*p)-buf)+(ptrdiff_t)(l) > (ptrdiff_t)len) luaL_error(L, "bad code"); (*p) += (l); + +#define mar_next_len(l,T) \ + if (((*p)-buf)+(ptrdiff_t)sizeof(T) > (ptrdiff_t)len) luaL_error(L, "bad code"); \ + l = *(T*)*p; (*p) += sizeof(T); + +static void mar_decode_value + (lua_State *L, const char *buf, size_t len, const char **p, size_t *idx) +{ + size_t l; + char val_type = **p; + mar_incr_ptr(MAR_CHR); + switch (val_type) { + case LUA_TBOOLEAN: + lua_pushboolean(L, *(char*)*p); + mar_incr_ptr(MAR_CHR); + break; + case LUA_TNUMBER: + lua_pushnumber(L, *(lua_Number*)*p); + mar_incr_ptr(MAR_I64); + break; + case LUA_TSTRING: + mar_next_len(l, uint32_t); + lua_pushlstring(L, *p, l); + mar_incr_ptr(l); + break; + case LUA_TTABLE: { + char tag = *(char*)*p; + mar_incr_ptr(MAR_CHR); + if (tag == MAR_TREF) { + int ref; + mar_next_len(ref, int); + lua_rawgeti(L, SEEN_IDX, ref); + } + else if (tag == MAR_TVAL) { + mar_next_len(l, uint32_t); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_rawseti(L, SEEN_IDX, (*idx)++); + mar_decode_table(L, *p, l, idx); + mar_incr_ptr(l); + } + else if (tag == MAR_TUSR) { + mar_next_len(l, uint32_t); + lua_newtable(L); + mar_decode_table(L, *p, l, idx); + lua_rawgeti(L, -1, 1); + lua_call(L, 0, 1); + lua_remove(L, -2); + lua_pushvalue(L, -1); + lua_rawseti(L, SEEN_IDX, (*idx)++); + mar_incr_ptr(l); + } + else { + luaL_error(L, "bad encoded data"); + } + break; + } + case LUA_TFUNCTION: { + unsigned int nups; + unsigned int i; + mar_Buffer dec_buf; + char tag = *(char*)*p; + mar_incr_ptr(1); + if (tag == MAR_TREF) { + int ref; + mar_next_len(ref, int); + lua_rawgeti(L, SEEN_IDX, ref); + } + else { + mar_next_len(l, uint32_t); + dec_buf.data = (char*)*p; + dec_buf.size = l; + dec_buf.head = l; + dec_buf.seek = 0; + lua_load(L, (lua_Reader)buf_read, &dec_buf, "=marshal", NULL); + mar_incr_ptr(l); + + lua_pushvalue(L, -1); + lua_rawseti(L, SEEN_IDX, (*idx)++); + + mar_next_len(l, uint32_t); + lua_newtable(L); + mar_decode_table(L, *p, l, idx); + + lua_pushstring(L, MAR_ENV_IDX_KEY); + lua_rawget(L, -2); + if (lua_isnumber(L, -1)) { + lua_pushglobaltable(L); + lua_rawset(L, -3); + } + else { + lua_pop(L, 1); + } + + lua_pushstring(L, MAR_NUPS_IDX_KEY); + lua_rawget(L, -2); + nups = luaL_checknumber(L, -1); + lua_pop(L, 1); + + for (i = 1; i <= nups; i++) { + lua_rawgeti(L, -1, i); + lua_setupvalue(L, -3, i); + } + + lua_pop(L, 1); + mar_incr_ptr(l); + } + break; + } + case LUA_TUSERDATA: { + char tag = *(char*)*p; + mar_incr_ptr(MAR_CHR); + if (tag == MAR_TREF) { + int ref; + mar_next_len(ref, int); + lua_rawgeti(L, SEEN_IDX, ref); + } + else if (tag == MAR_TUSR) { + mar_next_len(l, uint32_t); + lua_newtable(L); + mar_decode_table(L, *p, l, idx); + lua_rawgeti(L, -1, 1); + lua_call(L, 0, 1); + lua_remove(L, -2); + lua_pushvalue(L, -1); + lua_rawseti(L, SEEN_IDX, (*idx)++); + mar_incr_ptr(l); + } + else { /* tag == MAR_TVAL */ + lua_pushnil(L); + } + break; + } + case LUA_TNIL: + case LUA_TTHREAD: + lua_pushnil(L); + break; + default: + luaL_error(L, "bad code"); + } +} + +static int mar_decode_table(lua_State *L, const char* buf, size_t len, size_t *idx) +{ + const char* p; + p = buf; + while (p - buf < (ptrdiff_t)len) { + mar_decode_value(L, buf, len, &p, idx); + mar_decode_value(L, buf, len, &p, idx); + lua_settable(L, -3); + } + return 1; +} + +int mar_encode(lua_State* L) +{ + const unsigned char m = MAR_MAGIC; + size_t idx, len; + mar_Buffer buf; + + if (lua_isnone(L, 1)) { + lua_pushnil(L); + } + if (lua_isnoneornil(L, 2)) { + lua_newtable(L); + } + else if (!lua_istable(L, 2)) { + luaL_error(L, "bad argument #2 to encode (expected table)"); + } + lua_settop(L, 2); + + len = lua_rawlen(L, 2); + lua_newtable(L); + for (idx = 1; idx <= len; idx++) { + lua_rawgeti(L, 2, idx); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + continue; + } + lua_pushinteger(L, idx); + lua_rawset(L, SEEN_IDX); + } + lua_pushvalue(L, 1); + + buf_init(L, &buf); + buf_write(L, (const char*)&m, 1, &buf); + + mar_encode_value(L, &buf, -1, &idx); + + lua_pop(L, 1); + + lua_pushlstring(L, buf.data, buf.head); + + buf_done(L, &buf); + + lua_remove(L, SEEN_IDX); + + return 1; +} + +int mar_decode(lua_State* L) +{ + size_t l, idx, len; + const char *p; + const char *s = luaL_checklstring(L, 1, &l); + + if (l < 1) luaL_error(L, "bad header"); + if (*(unsigned char *)s++ != MAR_MAGIC) luaL_error(L, "bad magic"); + l -= 1; + + if (lua_isnoneornil(L, 2)) { + lua_newtable(L); + } + else if (!lua_istable(L, 2)) { + luaL_error(L, "bad argument #2 to decode (expected table)"); + } + lua_settop(L, 2); + + len = lua_rawlen(L, 2); + lua_newtable(L); + for (idx = 1; idx <= len; idx++) { + lua_rawgeti(L, 2, idx); + lua_rawseti(L, SEEN_IDX, idx); + } + + p = s; + mar_decode_value(L, s, l, &p, &idx); + + lua_remove(L, SEEN_IDX); + lua_remove(L, 2); + + return 1; +} + +int mar_clone(lua_State* L) +{ + mar_encode(L); + lua_replace(L, 1); + mar_decode(L); + return 1; +} + +static const luaL_Reg R[] = +{ + {"encode", mar_encode}, + {"decode", mar_decode}, + {"clone", mar_clone}, + {NULL, NULL} +}; + +int luaopen_marshal(lua_State *L) +{ + lua_newtable(L); + luaL_setfuncs(L, R, 0); + return 1; +} + diff --git a/lmarshal.h b/lmarshal.h new file mode 100644 index 0000000..5920add --- /dev/null +++ b/lmarshal.h @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2015 Eluna Lua Engine + * This program is free software licensed under GPL version 3 + * Please see the included DOCS/LICENSE.md for more information + */ + +extern "C" { +#include "lua.h" +} + +int mar_encode(lua_State* L); +int mar_decode(lua_State* L);