mirror of
https://github.com/SapphireServer/Sapphire.git
synced 2025-04-24 13:47:46 +00:00
script debug command, switch script info to a class
This commit is contained in:
parent
7728cda6e4
commit
d58994fe9b
11 changed files with 199 additions and 41 deletions
|
@ -3,7 +3,7 @@
|
||||||
class ActionSprint : public ActionScript
|
class ActionSprint : public ActionScript
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ActionSprint() : ActionScript( "ActionSprint", 3 )
|
ActionSprint() : ActionScript( "ActionSprint", 3 )
|
||||||
{}
|
{}
|
||||||
|
|
||||||
virtual void onCastFinish( Core::Entity::Player& player, Core::Entity::Actor& targetActor )
|
virtual void onCastFinish( Core::Entity::Player& player, Core::Entity::Actor& targetActor )
|
||||||
|
|
|
@ -5,6 +5,25 @@ class StatusSprint : public StatusEffectScript
|
||||||
public:
|
public:
|
||||||
StatusSprint() : StatusEffectScript( "StatusSprint", 50 )
|
StatusSprint() : StatusEffectScript( "StatusSprint", 50 )
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
virtual void onTick( Entity::Actor& actor ) override
|
||||||
|
{
|
||||||
|
if( actor.isPlayer() )
|
||||||
|
actor.getAsPlayer()->sendDebug( "tick tock bitch" );
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void onApply( Entity::Actor& actor ) override
|
||||||
|
{
|
||||||
|
if( actor.isPlayer() )
|
||||||
|
actor.getAsPlayer()->sendDebug( "status50 applied" );
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void onExpire( Entity::Actor& actor ) override
|
||||||
|
{
|
||||||
|
if( actor.isPlayer() )
|
||||||
|
actor.getAsPlayer()->sendDebug( "status50 timed out" );
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
EXPORT_STATUSEFFECTSCRIPT( StatusSprint )
|
EXPORT_STATUSEFFECTSCRIPT( StatusSprint )
|
|
@ -31,6 +31,8 @@
|
||||||
#include "Session.h"
|
#include "Session.h"
|
||||||
#include <boost/make_shared.hpp>
|
#include <boost/make_shared.hpp>
|
||||||
|
|
||||||
|
#include "Script/NativeScript.h"
|
||||||
|
|
||||||
|
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
|
||||||
|
@ -48,11 +50,11 @@ Core::DebugCommandHandler::DebugCommandHandler()
|
||||||
registerCommand( "add", &DebugCommandHandler::add, "Loads and injects a premade Packet.", 1 );
|
registerCommand( "add", &DebugCommandHandler::add, "Loads and injects a premade Packet.", 1 );
|
||||||
registerCommand( "inject", &DebugCommandHandler::injectPacket, "Loads and injects a premade packet.", 1 );
|
registerCommand( "inject", &DebugCommandHandler::injectPacket, "Loads and injects a premade packet.", 1 );
|
||||||
registerCommand( "injectc", &DebugCommandHandler::injectChatPacket, "Loads and injects a premade chat packet.", 1 );
|
registerCommand( "injectc", &DebugCommandHandler::injectChatPacket, "Loads and injects a premade chat packet.", 1 );
|
||||||
registerCommand( "script_reload", &DebugCommandHandler::scriptReload, "Reload all server scripts", 1 );
|
|
||||||
registerCommand( "nudge", &DebugCommandHandler::nudge, "Nudges you forward/up/down", 1 );
|
registerCommand( "nudge", &DebugCommandHandler::nudge, "Nudges you forward/up/down", 1 );
|
||||||
registerCommand( "info", &DebugCommandHandler::serverInfo, "Send server info", 0 );
|
registerCommand( "info", &DebugCommandHandler::serverInfo, "Send server info", 0 );
|
||||||
registerCommand( "unlock", &DebugCommandHandler::unlockCharacter, "Unlock character", 1 );
|
registerCommand( "unlock", &DebugCommandHandler::unlockCharacter, "Unlock character", 1 );
|
||||||
registerCommand( "help", &DebugCommandHandler::help, "Shows registered commands", 0 );
|
registerCommand( "help", &DebugCommandHandler::help, "Shows registered commands", 0 );
|
||||||
|
registerCommand( "script", &DebugCommandHandler::script, "Server script utilities", 1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear all loaded commands
|
// clear all loaded commands
|
||||||
|
@ -117,13 +119,6 @@ void Core::DebugCommandHandler::execCommand( char * data, Entity::Player& player
|
||||||
// Definition of the commands
|
// Definition of the commands
|
||||||
///////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
void Core::DebugCommandHandler::scriptReload( char * data, Entity::Player& player,
|
|
||||||
boost::shared_ptr< DebugCommand > command )
|
|
||||||
{
|
|
||||||
g_scriptMgr.reload();
|
|
||||||
player.sendDebug( "Scripts reloaded." );
|
|
||||||
}
|
|
||||||
|
|
||||||
void Core::DebugCommandHandler::help( char* data, Entity::Player& player, boost::shared_ptr< DebugCommand > command )
|
void Core::DebugCommandHandler::help( char* data, Entity::Player& player, boost::shared_ptr< DebugCommand > command )
|
||||||
{
|
{
|
||||||
player.sendDebug( "Registered debug commands:" );
|
player.sendDebug( "Registered debug commands:" );
|
||||||
|
@ -519,5 +514,73 @@ void Core::DebugCommandHandler::serverInfo( char * data, Entity::Player& player,
|
||||||
|
|
||||||
void Core::DebugCommandHandler::unlockCharacter( char* data, Entity::Player& player, boost::shared_ptr< DebugCommand > command )
|
void Core::DebugCommandHandler::unlockCharacter( char* data, Entity::Player& player, boost::shared_ptr< DebugCommand > command )
|
||||||
{
|
{
|
||||||
player.unlock( );
|
player.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Core::DebugCommandHandler::script( char* data, Entity::Player &player, boost::shared_ptr< DebugCommand > command )
|
||||||
|
{
|
||||||
|
std::string subCommand;
|
||||||
|
std::string params = "";
|
||||||
|
|
||||||
|
// check if the command has parameters
|
||||||
|
std::string tmpCommand = std::string( data + command->getName().length() + 1 );
|
||||||
|
|
||||||
|
std::size_t pos = tmpCommand.find_first_of( " " );
|
||||||
|
|
||||||
|
if( pos != std::string::npos )
|
||||||
|
// command has parameters, grab the first part
|
||||||
|
subCommand = tmpCommand.substr( 0, pos );
|
||||||
|
else
|
||||||
|
// no subcommand given
|
||||||
|
subCommand = tmpCommand;
|
||||||
|
|
||||||
|
// todo: fix params so it's empty if there's no params
|
||||||
|
if( command->getName().length() + 1 + pos + 1 < strlen( data ) )
|
||||||
|
params = std::string( data + command->getName().length() + 1 + pos + 1 );
|
||||||
|
|
||||||
|
g_log.debug( "[" + std::to_string( player.getId() ) + "] " +
|
||||||
|
"subCommand " + subCommand + " params: " + params );
|
||||||
|
|
||||||
|
if( subCommand == "unload" )
|
||||||
|
{
|
||||||
|
if ( subCommand == params )
|
||||||
|
player.sendDebug( "Command failed: requires name of script" );
|
||||||
|
else
|
||||||
|
g_scriptMgr.getNativeScriptHandler().unloadScript( params );
|
||||||
|
}
|
||||||
|
else if( subCommand == "find" || subCommand == "f" )
|
||||||
|
{
|
||||||
|
if( subCommand == params )
|
||||||
|
player.sendDebug( "Because reasons of filling chat with nonsense, please enter a search term" );
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if( subCommand == "load" || subCommand == "l" )
|
||||||
|
{
|
||||||
|
if( subCommand == params )
|
||||||
|
player.sendDebug( "Command failed: requires relative path to script" );
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ( g_scriptMgr.getNativeScriptHandler().loadScript( params ) )
|
||||||
|
player.sendDebug( "Loaded '" + params + "' successfully" );
|
||||||
|
else
|
||||||
|
player.sendDebug( "Failed to load '" + params + "'" );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if( subCommand == "build" || subCommand == "b" )
|
||||||
|
{
|
||||||
|
if( subCommand == params )
|
||||||
|
player.sendDebug( "Command failed: requires name of cmake target" );
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
player.sendDebug( "Unknown script subcommand: " + subCommand );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,8 @@ public:
|
||||||
void unlockCharacter( char* data, Entity::Player& player, boost::shared_ptr< DebugCommand > command );
|
void unlockCharacter( char* data, Entity::Player& player, boost::shared_ptr< DebugCommand > command );
|
||||||
void targetInfo( char* data, Entity::Player& player, boost::shared_ptr< DebugCommand > command );
|
void targetInfo( char* data, Entity::Player& player, boost::shared_ptr< DebugCommand > command );
|
||||||
|
|
||||||
|
void script( char* data, Entity::Player& player, boost::shared_ptr< DebugCommand > command );
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,38 +71,49 @@ namespace Core {
|
||||||
m_battleNpcScripts.erase( npcId );
|
m_battleNpcScripts.erase( npcId );
|
||||||
}
|
}
|
||||||
|
|
||||||
void NativeScript::loadScript( std::string path )
|
bool NativeScript::loadScript( std::string path )
|
||||||
{
|
{
|
||||||
auto handle = m_loader.loadModule( path );
|
auto info = m_loader.loadModule( path );
|
||||||
if( handle )
|
if( info )
|
||||||
{
|
{
|
||||||
// todo: this is shit
|
// todo: this is shit
|
||||||
if( auto script = m_loader.getScriptObject< StatusEffectScript >( handle, "StatusEffectScript" ) )
|
if( auto script = m_loader.getScriptObject< StatusEffectScript >( info->handle, "StatusEffectScript" ) )
|
||||||
{
|
{
|
||||||
|
info->script = script;
|
||||||
m_statusEffectScripts[ script->getId() ] = script;
|
m_statusEffectScripts[ script->getId() ] = script;
|
||||||
}
|
}
|
||||||
else if( auto script = m_loader.getScriptObject< ActionScript >( handle, "ActionScript" ) )
|
else if( auto script = m_loader.getScriptObject< ActionScript >( info->handle, "ActionScript" ) )
|
||||||
{
|
{
|
||||||
|
info->script = script;
|
||||||
m_actionScripts[ script->getId() ] = script;
|
m_actionScripts[ script->getId() ] = script;
|
||||||
}
|
}
|
||||||
else if( auto script = m_loader.getScriptObject< QuestScript >( handle, "QuestScript" ) )
|
else if( auto script = m_loader.getScriptObject< QuestScript >( info->handle, "QuestScript" ) )
|
||||||
{
|
{
|
||||||
|
info->script = script;
|
||||||
m_questScripts[ script->getId() ] = script;
|
m_questScripts[ script->getId() ] = script;
|
||||||
}
|
}
|
||||||
else if( auto script = m_loader.getScriptObject< BattleNpcScript >( handle, "BattleNpcScript" ) )
|
else if( auto script = m_loader.getScriptObject< BattleNpcScript >( info->handle, "BattleNpcScript" ) )
|
||||||
{
|
{
|
||||||
|
info->script = script;
|
||||||
m_battleNpcScripts[ script->getId() ] = script;
|
m_battleNpcScripts[ script->getId() ] = script;
|
||||||
}
|
}
|
||||||
else if( auto script = m_loader.getScriptObject< ZoneScript >( handle, "ZoneScript" ) )
|
else if( auto script = m_loader.getScriptObject< ZoneScript >( info->handle, "ZoneScript" ) )
|
||||||
{
|
{
|
||||||
|
info->script = script;
|
||||||
m_zoneScripts[ script->getId() ] = script;
|
m_zoneScripts[ script->getId() ] = script;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// unload anything which doesn't have a suitable export
|
// unload anything which doesn't have a suitable export
|
||||||
m_loader.unloadScript( handle );
|
m_loader.unloadScript( info->handle );
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string NativeScript::getModuleExtension()
|
const std::string NativeScript::getModuleExtension()
|
||||||
|
@ -110,10 +121,15 @@ namespace Core {
|
||||||
return m_loader.getModuleExtension();
|
return m_loader.getModuleExtension();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NativeScript::unloadScript( std::string name )
|
||||||
|
{
|
||||||
|
return m_loader.unloadScript( name );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
boost::shared_ptr< NativeScript > create_script_engine( )
|
boost::shared_ptr< NativeScript > create_script_engine( )
|
||||||
{
|
{
|
||||||
return boost::make_shared< NativeScript >( );
|
return boost::make_shared< NativeScript >();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
|
|
||||||
#include <Server_Common/Crypt/md5.h>
|
#include <Server_Common/Crypt/md5.h>
|
||||||
|
|
||||||
#include "NativeScriptApi.h"
|
|
||||||
#include "ScriptLoader.h"
|
#include "ScriptLoader.h"
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
|
@ -41,11 +40,12 @@ namespace Core {
|
||||||
void removeBattleNpcScript( uint32_t npcId );
|
void removeBattleNpcScript( uint32_t npcId );
|
||||||
|
|
||||||
|
|
||||||
void loadScript( std::string );
|
bool loadScript( std::string );
|
||||||
void unloadScript( std::string );
|
bool unloadScript( std::string );
|
||||||
void clearAllScripts();
|
void clearAllScripts();
|
||||||
|
|
||||||
const std::string getModuleExtension();
|
const std::string getModuleExtension();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
29
src/servers/Server_Zone/Script/ScriptInfo.h
Normal file
29
src/servers/Server_Zone/Script/ScriptInfo.h
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#ifndef SAPPHIRE_SCRIPTINFO_H
|
||||||
|
#define SAPPHIRE_SCRIPTINFO_H
|
||||||
|
|
||||||
|
#include "NativeScriptApi.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
typedef HMODULE ModuleHandle;
|
||||||
|
#else
|
||||||
|
typedef void* ModuleHandle;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
namespace Scripting {
|
||||||
|
|
||||||
|
class ScriptInfo
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ScriptInfo() = default;
|
||||||
|
|
||||||
|
std::string library_name;
|
||||||
|
ModuleHandle handle;
|
||||||
|
ScriptObject* script;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif //SAPPHIRE_SCRIPTINFO_H
|
|
@ -6,6 +6,9 @@
|
||||||
|
|
||||||
extern Core::Logger g_log;
|
extern Core::Logger g_log;
|
||||||
|
|
||||||
|
Core::Scripting::ScriptLoader::ScriptLoader()
|
||||||
|
{}
|
||||||
|
|
||||||
const std::string Core::Scripting::ScriptLoader::getModuleExtension()
|
const std::string Core::Scripting::ScriptLoader::getModuleExtension()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
@ -37,7 +40,7 @@ bool Core::Scripting::ScriptLoader::unloadModule( ModuleHandle handle )
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModuleHandle Core::Scripting::ScriptLoader::loadModule( std::string path )
|
Core::Scripting::ScriptInfo* Core::Scripting::ScriptLoader::loadModule( std::string path )
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
ModuleHandle handle = LoadLibrary( path.c_str() );
|
ModuleHandle handle = LoadLibrary( path.c_str() );
|
||||||
|
@ -49,15 +52,20 @@ ModuleHandle Core::Scripting::ScriptLoader::loadModule( std::string path )
|
||||||
{
|
{
|
||||||
g_log.error( "Failed to load module from: " + path );
|
g_log.error( "Failed to load module from: " + path );
|
||||||
|
|
||||||
return NULL;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_log.info( "Loaded module from '" + path + "' @ 0x" + boost::str( boost::format( "%|08X|" ) % handle ) );
|
g_log.info( "Loaded module from '" + path + "' @ 0x" + boost::str( boost::format( "%|08X|" ) % handle ) );
|
||||||
|
|
||||||
boost::filesystem::path f( path );
|
boost::filesystem::path f( path );
|
||||||
m_moduleMap.insert( std::make_pair( f.stem().string(), handle ) );
|
|
||||||
|
|
||||||
return handle;
|
auto info = new ScriptInfo;
|
||||||
|
info->handle = handle;
|
||||||
|
info->library_name = f.stem().string();
|
||||||
|
|
||||||
|
m_scriptMap.insert( std::make_pair( f.stem().string(), info ) );
|
||||||
|
|
||||||
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScriptObject* Core::Scripting::ScriptLoader::getScriptObjectExport( ModuleHandle handle, std::string name )
|
ScriptObject* Core::Scripting::ScriptLoader::getScriptObjectExport( ModuleHandle handle, std::string name )
|
||||||
|
@ -88,26 +96,39 @@ ScriptObject* Core::Scripting::ScriptLoader::getScriptObjectExport( ModuleHandle
|
||||||
|
|
||||||
bool Core::Scripting::ScriptLoader::unloadScript( std::string name )
|
bool Core::Scripting::ScriptLoader::unloadScript( std::string name )
|
||||||
{
|
{
|
||||||
auto moduleHandle = m_moduleMap.at( name );
|
auto info = m_scriptMap.find( name );
|
||||||
if( moduleHandle )
|
if( info == m_scriptMap.end() )
|
||||||
{
|
return false;
|
||||||
return unloadModule( moduleHandle );
|
|
||||||
}
|
|
||||||
|
|
||||||
g_log.info( "Module '" + name + "' is not loaded" );
|
return unloadScript( info->second->handle );
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Core::Scripting::ScriptLoader::unloadScript( ModuleHandle handle )
|
bool Core::Scripting::ScriptLoader::unloadScript( ModuleHandle handle )
|
||||||
{
|
{
|
||||||
for( auto it = m_moduleMap.begin(); it != m_moduleMap.end(); ++it )
|
for( auto it = m_scriptMap.begin(); it != m_scriptMap.end(); ++it )
|
||||||
{
|
{
|
||||||
if( it->second == handle )
|
if( it->second->handle == handle )
|
||||||
{
|
{
|
||||||
m_moduleMap.erase( it );
|
delete it->second;
|
||||||
|
m_scriptMap.erase( it );
|
||||||
|
|
||||||
return unloadModule( handle );
|
return unloadModule( handle );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::string& Core::Scripting::ScriptLoader::getModuleNameFromHandle( ModuleHandle handle ) const
|
||||||
|
{
|
||||||
|
for( auto it = m_scriptMap.begin(); it != m_scriptMap.end(); ++it )
|
||||||
|
{
|
||||||
|
if( it->second->handle == handle )
|
||||||
|
{
|
||||||
|
return it->first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nb: i'm not sure how this would ever be reached but you know
|
||||||
|
return "";
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "NativeScriptApi.h"
|
#include "NativeScriptApi.h"
|
||||||
|
#include "ScriptInfo.h"
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
@ -18,18 +19,20 @@ namespace Scripting {
|
||||||
|
|
||||||
class ScriptLoader {
|
class ScriptLoader {
|
||||||
protected:
|
protected:
|
||||||
std::unordered_map< std::string, ModuleHandle > m_moduleMap;
|
std::unordered_map< std::string, ScriptInfo* > m_scriptMap;
|
||||||
|
|
||||||
bool unloadModule( ModuleHandle );
|
bool unloadModule( ModuleHandle );
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ScriptLoader() = default;
|
ScriptLoader();
|
||||||
|
|
||||||
const std::string getModuleExtension();
|
const std::string getModuleExtension();
|
||||||
ModuleHandle loadModule( std::string );
|
ScriptInfo* loadModule( std::string );
|
||||||
bool unloadScript( std::string );
|
bool unloadScript( std::string );
|
||||||
bool unloadScript( ModuleHandle );
|
bool unloadScript( ModuleHandle );
|
||||||
|
|
||||||
|
const std::string& getModuleNameFromHandle( ModuleHandle handle ) const;
|
||||||
|
|
||||||
ScriptObject* getScriptObjectExport( ModuleHandle handle, std::string name );
|
ScriptObject* getScriptObjectExport( ModuleHandle handle, std::string name );
|
||||||
|
|
||||||
template< typename T >
|
template< typename T >
|
||||||
|
|
|
@ -453,3 +453,8 @@ bool Core::Scripting::ScriptManager::onZoneInit( ZonePtr pZone )
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Scripting::NativeScript& Core::Scripting::ScriptManager::getNativeScriptHandler()
|
||||||
|
{
|
||||||
|
return *m_nativeScriptHandler;
|
||||||
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ namespace Core
|
||||||
|
|
||||||
void loadDir( std::string dirname, std::set<std::string>& files, std::string ext );
|
void loadDir( std::string dirname, std::set<std::string>& files, std::string ext );
|
||||||
|
|
||||||
|
NativeScript& getNativeScriptHandler();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue