2014-04-26 07:41:03 +08:00
|
|
|
// The MIT License (MIT)
|
|
|
|
|
|
|
|
// Copyright (c) 2013 Danny Y., Rapptz
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
|
|
|
#ifndef SOL_USERDATA_HPP
|
|
|
|
#define SOL_USERDATA_HPP
|
|
|
|
|
2014-04-26 08:53:36 +08:00
|
|
|
#include "state.hpp"
|
2014-04-26 10:05:58 +08:00
|
|
|
#include "function_types.hpp"
|
2014-06-07 10:54:45 +08:00
|
|
|
#include "userdata_traits.hpp"
|
2014-06-27 16:25:57 +08:00
|
|
|
#include "default_construct.hpp"
|
2014-04-26 07:41:03 +08:00
|
|
|
#include <vector>
|
2014-06-08 04:33:39 +08:00
|
|
|
#include <array>
|
|
|
|
#include <algorithm>
|
2014-04-26 07:41:03 +08:00
|
|
|
|
|
|
|
namespace sol {
|
2014-04-26 08:53:36 +08:00
|
|
|
namespace detail {
|
|
|
|
template<typename T, typename... Args>
|
|
|
|
inline std::unique_ptr<T> make_unique(Args&&... args) {
|
|
|
|
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
|
|
|
|
}
|
|
|
|
} // detail
|
2014-04-26 07:41:03 +08:00
|
|
|
|
2014-04-27 06:23:56 +08:00
|
|
|
template<typename T>
|
2014-04-26 07:41:03 +08:00
|
|
|
class userdata {
|
|
|
|
private:
|
2014-07-28 03:56:24 +08:00
|
|
|
typedef std::unordered_map<std::string, std::pair<std::unique_ptr<base_function>, bool>> function_map_t;
|
|
|
|
const static std::array<std::string, 2> metavariablenames;
|
2014-06-08 04:33:39 +08:00
|
|
|
const static std::array<std::string, 19> metafunctionnames;
|
2014-07-28 03:56:24 +08:00
|
|
|
function_map_t indexmetafunctions, newindexmetafunctions;
|
2014-04-26 07:41:03 +08:00
|
|
|
std::vector<std::string> functionnames;
|
2014-07-28 03:56:24 +08:00
|
|
|
std::vector<std::unique_ptr<base_function>> metafunctions;
|
2014-06-08 04:33:39 +08:00
|
|
|
std::vector<luaL_Reg> metafunctiontable;
|
|
|
|
std::vector<luaL_Reg> ptrmetafunctiontable;
|
2014-06-28 12:27:48 +08:00
|
|
|
lua_CFunction cleanup;
|
2014-07-28 03:56:24 +08:00
|
|
|
std::string luaname;
|
2014-04-27 12:53:57 +08:00
|
|
|
|
2014-04-27 06:23:56 +08:00
|
|
|
template<typename... TTypes>
|
2014-04-26 07:41:03 +08:00
|
|
|
struct constructor {
|
2014-04-27 06:23:56 +08:00
|
|
|
template<typename... Args>
|
2014-04-27 12:53:57 +08:00
|
|
|
static void do_constructor(lua_State* L, T* obj, call_syntax syntax, int, types<Args...>) {
|
2014-06-27 16:34:16 +08:00
|
|
|
default_construct fx{};
|
2014-06-27 16:25:57 +08:00
|
|
|
stack::get_call(L, 1 + static_cast<int>(syntax), fx, types<Args...>(), obj);
|
2014-06-27 16:34:16 +08:00
|
|
|
}
|
2014-04-26 07:41:03 +08:00
|
|
|
|
2014-04-27 12:53:57 +08:00
|
|
|
static void match_constructor(lua_State*, T*, call_syntax, int) {
|
2014-05-22 10:24:15 +08:00
|
|
|
throw error("No matching constructor for the arguments provided");
|
2014-04-26 12:19:36 +08:00
|
|
|
}
|
2014-04-27 12:53:57 +08:00
|
|
|
|
2014-04-27 06:23:56 +08:00
|
|
|
template<typename ...CArgs, typename... Args>
|
|
|
|
static void match_constructor(lua_State* L, T* obj, call_syntax syntax, int argcount, types<CArgs...> t, Args&&... args) {
|
2014-04-26 12:19:36 +08:00
|
|
|
if (argcount == sizeof...(CArgs)) {
|
2014-04-27 06:23:56 +08:00
|
|
|
do_constructor(L, obj, syntax, argcount, t);
|
2014-04-26 12:19:36 +08:00
|
|
|
return;
|
|
|
|
}
|
2014-04-27 06:23:56 +08:00
|
|
|
match_constructor(L, obj, syntax, argcount, std::forward<Args>(args)...);
|
2014-04-26 12:19:36 +08:00
|
|
|
}
|
2014-04-26 07:41:03 +08:00
|
|
|
|
2014-04-26 12:19:36 +08:00
|
|
|
static int construct(lua_State* L) {
|
2014-04-27 17:08:39 +08:00
|
|
|
auto&& meta = userdata_traits<T>::metatable;
|
2014-04-27 06:23:56 +08:00
|
|
|
call_syntax syntax = stack::get_call_syntax(L, meta);
|
2014-04-26 12:19:36 +08:00
|
|
|
int argcount = lua_gettop(L);
|
2014-04-27 12:53:57 +08:00
|
|
|
|
2014-04-27 06:23:56 +08:00
|
|
|
void* udata = lua_newuserdata(L, sizeof(T));
|
|
|
|
T* obj = static_cast<T*>(udata);
|
2014-06-27 16:25:57 +08:00
|
|
|
match_constructor(L, obj, syntax, argcount - static_cast<int>(syntax), typename identity<TTypes>::type()...);
|
2014-04-27 12:53:57 +08:00
|
|
|
|
2014-06-09 17:30:00 +08:00
|
|
|
if (luaL_newmetatable(L, std::addressof(meta[0])) == 1) {
|
|
|
|
lua_pop(L, 1);
|
|
|
|
std::string err = "Unable to get userdata metatable for ";
|
|
|
|
err += meta;
|
|
|
|
throw error(err);
|
|
|
|
}
|
|
|
|
lua_setmetatable(L, -2);
|
2014-04-27 12:53:57 +08:00
|
|
|
|
2014-04-27 06:23:56 +08:00
|
|
|
return 1;
|
2014-04-26 07:41:03 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
struct destructor {
|
|
|
|
static int destruct(lua_State* L) {
|
2014-04-27 06:23:56 +08:00
|
|
|
userdata_t udata = stack::get<userdata_t>(L, 1);
|
2014-04-26 12:19:36 +08:00
|
|
|
T* obj = static_cast<T*>(udata.value);
|
2014-04-26 07:41:03 +08:00
|
|
|
std::allocator<T> alloc{};
|
|
|
|
alloc.destroy(obj);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2014-07-28 03:56:24 +08:00
|
|
|
template<std::size_t N>
|
|
|
|
void build_cleanup () {
|
|
|
|
cleanup = &base_function::userdata<N>::gc;
|
2014-06-29 14:16:48 +08:00
|
|
|
}
|
|
|
|
|
2014-07-28 03:56:24 +08:00
|
|
|
template<std::size_t N>
|
|
|
|
void build_function_tables(function_map_t*& index, function_map_t*& newindex) {
|
|
|
|
int extracount = 0;
|
|
|
|
if (!indexmetafunctions.empty()) {
|
|
|
|
if (index == nullptr) {
|
|
|
|
auto idxptr = detail::make_unique<userdata_indexing_function<void (T::*)(), T>>("__index", nullptr);
|
|
|
|
index = &(idxptr->functions);
|
|
|
|
functionnames.emplace_back("__index");
|
|
|
|
metafunctions.emplace_back(std::move(idxptr));
|
|
|
|
std::string& name = functionnames.back();
|
|
|
|
metafunctiontable.push_back( { name.c_str(), &base_function::userdata<N>::call } );
|
|
|
|
ptrmetafunctiontable.push_back( { name.c_str(), &base_function::userdata<N>::ref_call } );
|
|
|
|
++extracount;
|
|
|
|
}
|
|
|
|
auto& idx = *index;
|
|
|
|
for (auto&& namedfunc : indexmetafunctions ) {
|
|
|
|
idx.emplace(std::move(namedfunc.first), std::move(namedfunc.second));
|
|
|
|
}
|
2014-06-29 14:16:48 +08:00
|
|
|
}
|
2014-07-28 03:56:24 +08:00
|
|
|
if (!newindexmetafunctions.empty()) {
|
|
|
|
if (newindex == nullptr) {
|
|
|
|
auto idxptr = detail::make_unique<userdata_indexing_function<void (T::*)(), T>>("__newindex", nullptr);
|
|
|
|
newindex = &(idxptr->functions);
|
|
|
|
functionnames.emplace_back("__newindex");
|
|
|
|
metafunctions.emplace_back(std::move(idxptr));
|
|
|
|
std::string& name = functionnames.back();
|
|
|
|
if (extracount > 0) {
|
|
|
|
metafunctiontable.push_back( { name.c_str(), &base_function::userdata<N + 1>::call } );
|
|
|
|
ptrmetafunctiontable.push_back( { name.c_str(), &base_function::userdata<N + 1>::ref_call } );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
metafunctiontable.push_back( { name.c_str(), &base_function::userdata<N>::call } );
|
|
|
|
ptrmetafunctiontable.push_back( { name.c_str(), &base_function::userdata<N>::ref_call } );
|
|
|
|
}
|
|
|
|
++extracount;
|
|
|
|
}
|
|
|
|
auto& idx = *newindex;
|
|
|
|
for (auto&& namedfunc : newindexmetafunctions ) {
|
|
|
|
idx.emplace(std::move(namedfunc.first), std::move(namedfunc.second));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
switch (extracount) {
|
|
|
|
case 2:
|
|
|
|
build_cleanup<N + 2>();
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
build_cleanup<N + 1>();
|
|
|
|
break;
|
|
|
|
case 0:
|
|
|
|
default:
|
|
|
|
build_cleanup<N + 0>();
|
|
|
|
break;
|
2014-06-29 14:16:48 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-28 03:56:24 +08:00
|
|
|
template<std::size_t N, typename TBase, typename Ret>
|
|
|
|
bool build_function(std::true_type, function_map_t*&, function_map_t*&, std::string funcname, Ret TBase::* func) {
|
|
|
|
static_assert(std::is_base_of<TBase, T>::value, "Any registered function must be part of the class");
|
|
|
|
typedef typename std::decay<decltype(func)>::type function_type;
|
|
|
|
indexmetafunctions.emplace(funcname, std::make_pair(detail::make_unique<userdata_variable_function<function_type, T>>(func), false));
|
|
|
|
newindexmetafunctions.emplace(funcname, std::make_pair(detail::make_unique<userdata_variable_function<function_type, T>>(func), false));
|
|
|
|
return false;
|
|
|
|
}
|
2014-04-26 07:41:03 +08:00
|
|
|
|
2014-07-28 03:56:24 +08:00
|
|
|
template<std::size_t N, typename TBase, typename Ret>
|
|
|
|
bool build_function(std::false_type, function_map_t*& index, function_map_t*& newindex, std::string funcname, Ret TBase::* func) {
|
2014-05-26 01:46:23 +08:00
|
|
|
static_assert(std::is_base_of<TBase, T>::value, "Any registered function must be part of the class");
|
2014-04-27 15:25:47 +08:00
|
|
|
typedef typename std::decay<decltype(func)>::type function_type;
|
2014-07-28 03:56:24 +08:00
|
|
|
auto metamethod = std::find(metafunctionnames.begin(), metafunctionnames.end(), funcname);
|
2014-06-08 04:33:39 +08:00
|
|
|
if (metamethod != metafunctionnames.end()) {
|
2014-07-28 03:56:24 +08:00
|
|
|
functionnames.push_back(std::move(funcname));
|
|
|
|
std::string& name = functionnames.back();
|
|
|
|
auto indexmetamethod = std::find(metavariablenames.begin(), metavariablenames.end(), name);
|
|
|
|
std::unique_ptr<base_function> ptr(nullptr);
|
|
|
|
if (indexmetamethod != metavariablenames.end()) {
|
|
|
|
auto idxptr = detail::make_unique<userdata_indexing_function<function_type, T>>(name, func);
|
|
|
|
switch( std::distance(indexmetamethod, metavariablenames.end()) ) {
|
|
|
|
case 0:
|
|
|
|
index = &(idxptr->functions);
|
|
|
|
break;
|
|
|
|
case 1:
|
|
|
|
newindex = &(idxptr->functions);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
ptr = std::move(idxptr);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
ptr = detail::make_unique<userdata_function<function_type, T>>(func);
|
|
|
|
}
|
|
|
|
metafunctions.emplace_back(std::move(ptr));
|
|
|
|
metafunctiontable.push_back( { name.c_str(), &base_function::userdata<N>::call } );
|
|
|
|
ptrmetafunctiontable.push_back( { name.c_str(), &base_function::userdata<N>::ref_call } );
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
indexmetafunctions.emplace(funcname, std::make_pair(detail::make_unique<userdata_function<function_type, T>>(func), true ));
|
|
|
|
newindexmetafunctions.emplace(funcname, std::make_pair(detail::make_unique<userdata_function<function_type, T>>(func), true));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
template<std::size_t N, typename TBase, typename Ret, typename... Args>
|
|
|
|
void build_function_tables(function_map_t*& index, function_map_t*& newindex, std::string funcname, Ret TBase::* func, Args&&... args) {
|
|
|
|
typedef typename std::is_member_object_pointer<decltype(func)>::type is_variable;
|
|
|
|
static const std::size_t V = static_cast<std::size_t>( !is_variable::value );
|
|
|
|
if (build_function<N>(is_variable(), index, newindex, std::move(funcname), func)) {
|
|
|
|
build_function_tables<N + V>(index, newindex, std::forward<Args>(args)...);
|
2014-06-08 04:33:39 +08:00
|
|
|
}
|
|
|
|
else {
|
2014-07-28 03:56:24 +08:00
|
|
|
build_function_tables<N>(index, newindex, std::forward<Args>(args)...);
|
2014-06-08 04:33:39 +08:00
|
|
|
}
|
2014-04-26 07:41:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
public:
|
2014-04-27 06:23:56 +08:00
|
|
|
template<typename... Args>
|
2014-04-27 17:08:39 +08:00
|
|
|
userdata(Args&&... args): userdata(userdata_traits<T>::name, default_constructor, std::forward<Args>(args)...) {}
|
2014-04-26 07:41:03 +08:00
|
|
|
|
2014-04-27 06:23:56 +08:00
|
|
|
template<typename... Args, typename... CArgs>
|
2014-04-27 17:08:39 +08:00
|
|
|
userdata(constructors<CArgs...> c, Args&&... args): userdata(userdata_traits<T>::name, std::move(c), std::forward<Args>(args)...) {}
|
2014-04-26 07:41:03 +08:00
|
|
|
|
2014-04-27 06:23:56 +08:00
|
|
|
template<typename... Args, typename... CArgs>
|
2014-04-27 13:29:37 +08:00
|
|
|
userdata(std::string name, constructors<CArgs...>, Args&&... args): luaname(std::move(name)) {
|
2014-04-27 06:23:56 +08:00
|
|
|
functionnames.reserve(sizeof...(args) + 2);
|
2014-06-28 12:27:48 +08:00
|
|
|
metafunctiontable.reserve(sizeof...(args));
|
|
|
|
ptrmetafunctiontable.reserve(sizeof...(args));
|
|
|
|
|
2014-07-28 03:56:24 +08:00
|
|
|
function_map_t* index = nullptr;
|
|
|
|
function_map_t* newindex = nullptr;
|
|
|
|
build_function_tables<0>(index, newindex, std::forward<Args>(args)...);
|
|
|
|
indexmetafunctions.clear();
|
|
|
|
newindexmetafunctions.clear();
|
2014-04-26 07:41:03 +08:00
|
|
|
functionnames.push_back("new");
|
2014-07-28 03:56:24 +08:00
|
|
|
metafunctiontable.push_back({ functionnames.back().c_str(), &constructor<CArgs...>::construct });
|
2014-04-27 06:23:56 +08:00
|
|
|
functionnames.push_back("__gc");
|
2014-06-28 12:27:48 +08:00
|
|
|
metafunctiontable.push_back({ functionnames.back().c_str(), &destructor::destruct });
|
2014-06-07 10:54:45 +08:00
|
|
|
// ptr_functions does not participate in garbage collection/new,
|
|
|
|
// as all pointered types are considered
|
|
|
|
// to be references. This makes returns of
|
|
|
|
// `std::vector<int>&` and `std::vector<int>*` work
|
2014-04-27 12:53:57 +08:00
|
|
|
|
2014-07-28 03:56:24 +08:00
|
|
|
metafunctiontable.push_back( { nullptr, nullptr } );
|
|
|
|
ptrmetafunctiontable.push_back( { nullptr, nullptr } );
|
2014-04-26 07:41:03 +08:00
|
|
|
}
|
|
|
|
|
2014-04-27 13:29:37 +08:00
|
|
|
template<typename... Args, typename... CArgs>
|
|
|
|
userdata(const char* name, constructors<CArgs...> c, Args&&... args) :
|
|
|
|
userdata(std::string(name), std::move(c), std::forward<Args>(args)...) {}
|
2014-06-07 10:54:45 +08:00
|
|
|
|
|
|
|
const std::string& name () const {
|
|
|
|
return luaname;
|
|
|
|
}
|
2014-06-29 14:16:48 +08:00
|
|
|
|
|
|
|
void push (lua_State* L) {
|
|
|
|
// push pointer tables first,
|
2014-07-28 03:56:24 +08:00
|
|
|
// but leave the regular T table on last
|
|
|
|
// so it can be linked to a type for usage with `.new(...)` or `:new(...)`
|
2014-06-29 14:16:48 +08:00
|
|
|
push_metatable(L, userdata_traits<T*>::metatable,
|
2014-07-28 03:56:24 +08:00
|
|
|
metafunctions, ptrmetafunctiontable);
|
|
|
|
lua_pop(L, 1);
|
|
|
|
|
2014-06-29 14:16:48 +08:00
|
|
|
push_metatable(L, userdata_traits<T>::metatable,
|
2014-07-28 03:56:24 +08:00
|
|
|
metafunctions, metafunctiontable);
|
|
|
|
set_global_deleter(L);
|
|
|
|
}
|
|
|
|
private:
|
|
|
|
|
|
|
|
template <typename Meta, typename MetaFuncs, typename MetaFuncTable>
|
|
|
|
static void push_metatable(lua_State* L, Meta&& metakey, MetaFuncs&& metafuncs, MetaFuncTable&& metafunctable) {
|
|
|
|
luaL_newmetatable(L, std::addressof(metakey[0]));
|
|
|
|
if (metafunctable.size() > 1) {
|
|
|
|
// regular functions accessed through __index semantics
|
|
|
|
int up = push_upvalues(L, metafuncs);
|
|
|
|
luaL_setfuncs(L, metafunctable.data(), up);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void set_global_deleter (lua_State* L) {
|
2014-06-29 14:16:48 +08:00
|
|
|
// Automatic deleter table -- stays alive until lua VM dies
|
|
|
|
// even if the user calls collectgarbage()
|
|
|
|
lua_createtable(L, 0, 0);
|
|
|
|
lua_createtable(L, 0, 1);
|
2014-07-28 03:56:24 +08:00
|
|
|
int up = push_upvalues<true>(L, metafunctions);
|
2014-06-29 14:16:48 +08:00
|
|
|
lua_pushcclosure(L, cleanup, up);
|
|
|
|
lua_setfield(L, -2, "__gc");
|
|
|
|
lua_setmetatable(L, -2);
|
|
|
|
// gctable name by default has ♻ part of it
|
|
|
|
lua_setglobal(L, std::addressof(userdata_traits<T>::gctable[0]));
|
|
|
|
}
|
2014-07-28 03:56:24 +08:00
|
|
|
|
|
|
|
template <bool release = false, typename TCont>
|
|
|
|
static int push_upvalues (lua_State* L, TCont&& cont) {
|
|
|
|
int n = 0;
|
|
|
|
for (auto& c : cont) {
|
|
|
|
if (release)
|
|
|
|
stack::push<upvalue_t>(L, c.release());
|
|
|
|
else
|
|
|
|
stack::push<upvalue_t>(L, c.get());
|
|
|
|
++n;
|
|
|
|
}
|
|
|
|
return n;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
const std::array<std::string, 2> userdata<T>::metavariablenames = {
|
|
|
|
"__index",
|
|
|
|
"__newindex"
|
2014-06-07 10:54:45 +08:00
|
|
|
};
|
|
|
|
|
2014-06-08 04:33:39 +08:00
|
|
|
template <typename T>
|
|
|
|
const std::array<std::string, 19> userdata<T>::metafunctionnames = {
|
|
|
|
"__index",
|
|
|
|
"__newindex",
|
|
|
|
"__mode",
|
|
|
|
"__call",
|
|
|
|
"__metatable",
|
|
|
|
"__tostring",
|
|
|
|
"__len",
|
|
|
|
"__gc",
|
|
|
|
"__unm",
|
|
|
|
"__add",
|
|
|
|
"__sub",
|
|
|
|
"__mul",
|
|
|
|
"__div",
|
|
|
|
"__mod",
|
|
|
|
"__pow",
|
|
|
|
"__concat",
|
|
|
|
"__eq",
|
|
|
|
"__lt",
|
|
|
|
"__le",
|
|
|
|
};
|
|
|
|
|
2014-06-07 10:54:45 +08:00
|
|
|
namespace stack {
|
|
|
|
template <typename T>
|
|
|
|
struct pusher<userdata<T>> {
|
2014-06-08 04:33:39 +08:00
|
|
|
static void push (lua_State* L, userdata<T>& user) {
|
2014-06-29 14:16:48 +08:00
|
|
|
user.push(L);
|
2014-06-07 10:54:45 +08:00
|
|
|
}
|
2014-04-26 07:41:03 +08:00
|
|
|
};
|
2014-06-07 10:54:45 +08:00
|
|
|
} // stack
|
|
|
|
|
2014-04-27 13:29:37 +08:00
|
|
|
} // sol
|
2014-04-26 07:41:03 +08:00
|
|
|
|
2014-04-26 08:42:38 +08:00
|
|
|
#endif // SOL_USERDATA_HPP
|