From 94c1a50cc82eb07b2438410a1004b45457eb157e Mon Sep 17 00:00:00 2001 From: Chris Miles Date: Sun, 12 Sep 2021 22:08:04 -0500 Subject: [PATCH] [GM Command] Door Manipulation Command Port (#1524) * Initial commit * Push latest * Update door_manipulation.cpp * More door work * More doors work * Upload notes * Finalize changes * Remove comment * Add missing chat line * Swapped URI parser with something not using deprecated C++ functions --- common/CMakeLists.txt | 4 +- common/database.cpp | 67 ++ common/database.h | 2 + common/eqemu_logsys.cpp | 2 + common/eqemu_logsys.h | 2 + common/eqemu_logsys_log_aliases.h | 16 + common/http/uri.h | 633 ++++++++++++++ .../base/base_tool_game_objects_repository.h | 346 ++++++++ .../tool_game_objects_repository.h | 70 ++ .../generators/repository-generator.pl | 2 +- zone/CMakeLists.txt | 2 + zone/client.cpp | 10 + zone/client.h | 8 + zone/client_packet.cpp | 15 + zone/command.cpp | 6 + zone/command.h | 1 + zone/doors.cpp | 6 + zone/doors.h | 1 + zone/gm_commands/door_manipulation.cpp | 779 ++++++++++++++++++ zone/gm_commands/door_manipulation.h | 23 + zone/npc.cpp | 19 +- zone/npc.h | 2 +- 22 files changed, 2007 insertions(+), 9 deletions(-) create mode 100644 common/http/uri.h create mode 100644 common/repositories/base/base_tool_game_objects_repository.h create mode 100644 common/repositories/tool_game_objects_repository.h create mode 100644 zone/gm_commands/door_manipulation.cpp create mode 100644 zone/gm_commands/door_manipulation.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 6e8a826fc..e9de82316 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -526,6 +526,7 @@ SET(common_headers guild_base.h guilds.h http/httplib.h + http/uri.h inventory_profile.h inventory_slot.h ipc_mutex.h @@ -638,7 +639,8 @@ SET(common_headers StackWalker/StackWalker.h util/memory_stream.h util/directory.h - util/uuid.h) + util/uuid.h +) SOURCE_GROUP(Event FILES event/event_loop.h diff --git a/common/database.cpp b/common/database.cpp index cf9752350..0a5678b2c 100644 --- a/common/database.cpp +++ b/common/database.cpp @@ -39,6 +39,7 @@ #include "unix.h" #include #include + #endif #include "database.h" @@ -46,6 +47,8 @@ #include "extprofile.h" #include "string_util.h" #include "database_schema.h" +#include "http/httplib.h" +#include "http/uri.h" extern Client client; @@ -2418,3 +2421,67 @@ bool Database::CopyCharacter( return true; } +void Database::SourceDatabaseTableFromUrl(std::string table_name, std::string url) +{ + try { + uri request_uri(url); + + LogHTTP( + "[SourceDatabaseTableFromUrl] parsing url [{}] path [{}] host [{}] query_string [{}] protocol [{}] port [{}]", + url, + request_uri.get_path(), + request_uri.get_host(), + request_uri.get_query(), + request_uri.get_scheme(), + request_uri.get_port() + ); + + if (!DoesTableExist(table_name)) { + LogMySQLQuery("Table [{}] does not exist. Downloading from Github and installing...", table_name); + + // http get request + httplib::Client cli( + fmt::format( + "{}://{}", + request_uri.get_scheme(), + request_uri.get_host() + ).c_str() + ); + + cli.set_connection_timeout(0, 60000000); // 60 sec + cli.set_read_timeout(60, 0); // 60 seconds + cli.set_write_timeout(60, 0); // 60 seconds + + int sourced_queries = 0; + + if (auto res = cli.Get(request_uri.get_path().c_str())) { + if (res->status == 200) { + for (auto &s: SplitString(res->body, ';')) { + if (!trim(s).empty()) { + auto results = QueryDatabase(s); + if (!results.ErrorMessage().empty()) { + LogError("Error sourcing SQL [{}]", results.ErrorMessage()); + return; + } + sourced_queries++; + } + } + } + } + else { + LogError("Error retrieving URL [{}]", url); + } + + LogMySQLQuery( + "Table [{}] installed. Sourced [{}] queries", + table_name, + sourced_queries + ); + } + + } + catch (std::invalid_argument iae) { + LogError("[SourceDatabaseTableFromUrl] URI parser error [{}]", iae.what()); + } +} + diff --git a/common/database.h b/common/database.h index 79c52c514..e0eff4e8e 100644 --- a/common/database.h +++ b/common/database.h @@ -269,6 +269,8 @@ public: int CountInvSnapshots(); void ClearInvSnapshots(bool from_now = false); + void SourceDatabaseTableFromUrl(std::string table_name, std::string url); + private: diff --git a/common/eqemu_logsys.cpp b/common/eqemu_logsys.cpp index 83e9a45dd..e5f0c63c1 100644 --- a/common/eqemu_logsys.cpp +++ b/common/eqemu_logsys.cpp @@ -130,6 +130,8 @@ EQEmuLogSys *EQEmuLogSys::LoadLogSettingsDefaults() log_settings[Logs::Loot].log_to_gmsay = static_cast(Logs::General); log_settings[Logs::Scheduler].log_to_console = static_cast(Logs::General); log_settings[Logs::Cheat].log_to_console = static_cast(Logs::General); + log_settings[Logs::HTTP].log_to_console = static_cast(Logs::General); + log_settings[Logs::HTTP].log_to_gmsay = static_cast(Logs::General); /** * RFC 5424 diff --git a/common/eqemu_logsys.h b/common/eqemu_logsys.h index 6990764a4..3ec495413 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -125,6 +125,7 @@ namespace Logs { Cheat, ClientList, DiaWind, + HTTP, MaxCategoryID /* Don't Remove this */ }; @@ -208,6 +209,7 @@ namespace Logs { "Cheat", "ClientList", "DialogueWindow", + "HTTP", }; } diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index 10358b572..a949f63a8 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -676,6 +676,16 @@ OutF(LogSys, Logs::Detail, Logs::DiaWind, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ } while (0) +#define LogHTTP(message, ...) do {\ + if (LogSys.log_settings[Logs::HTTP].is_category_enabled == 1)\ + OutF(LogSys, Logs::General, Logs::HTTP, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogHTTPDetail(message, ...) do {\ + if (LogSys.log_settings[Logs::HTTP].is_category_enabled == 1)\ + OutF(LogSys, Logs::Detail, Logs::HTTP, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ if (LogSys.log_settings[log_category].is_category_enabled == 1)\ LogSys.Out(debug_level, log_category, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ @@ -1066,6 +1076,12 @@ #define LogDiaWindDetail(message, ...) do {\ } while (0) +#define LogHTTP(message, ...) do {\ +} while (0) + +#define LogHTTPDetail(message, ...) do {\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ } while (0) diff --git a/common/http/uri.h b/common/http/uri.h new file mode 100644 index 000000000..04b891e45 --- /dev/null +++ b/common/http/uri.h @@ -0,0 +1,633 @@ +// Copyright (C) 2015 Ben Lewis +// Licensed under the MIT license. +// https://github.com/ben-zen/uri-library + +#pragma once + +#include +#include +#include +#include +#include + +class uri { + /* URIs are broadly divided into two categories: hierarchical and + * non-hierarchical. Both hierarchical URIs and non-hierarchical URIs have a + * few elements in common; all URIs have a scheme of one or more alphanumeric + * characters followed by a colon, and they all may optionally have a query + * component preceded by a question mark, and a fragment component preceded by + * an octothorpe (hash mark: '#'). The query consists of stanzas separated by + * either ampersands ('&') or semicolons (';') (but only one or the other), + * and each stanza consists of a key and an optional value; if the value + * exists, the key and value must be divided by an equals sign. + * + * The following is an example from Wikipedia of a hierarchical URI: + * scheme:[//[user:password@]domain[:port]][/]path[?query][#fragment] + */ + +public: + + enum class scheme_category { + Hierarchical, + NonHierarchical + }; + + enum class component { + Scheme, + Content, + Username, + Password, + Host, + Port, + Path, + Query, + Fragment + }; + + enum class query_argument_separator { + ampersand, + semicolon + }; + + uri( + char const *uri_text, scheme_category category = scheme_category::Hierarchical, + query_argument_separator separator = query_argument_separator::ampersand + ) : + m_category(category), + m_path_is_rooted(false), + m_port(0), + m_separator(separator) + { + setup(std::string(uri_text), category); + }; + + uri( + std::string const &uri_text, scheme_category category = scheme_category::Hierarchical, + query_argument_separator separator = query_argument_separator::ampersand + ) : + m_category(category), + m_path_is_rooted(false), + m_port(0), + m_separator(separator) + { + setup(uri_text, category); + }; + + uri( + std::map const &components, + scheme_category category, + bool rooted_path, + query_argument_separator separator = query_argument_separator::ampersand + ) : + m_category(category), + m_path_is_rooted(rooted_path), + m_separator(separator) + { + if (components.count(component::Scheme)) { + if (components.at(component::Scheme).length() == 0) { + throw std::invalid_argument("Scheme cannot be empty."); + } + m_scheme = components.at(component::Scheme); + } + else { + throw std::invalid_argument("A URI must have a scheme."); + } + + if (category == scheme_category::Hierarchical) { + if (components.count(component::Content)) { + throw std::invalid_argument("The content component is only for use in non-hierarchical URIs."); + } + + bool has_username = components.count(component::Username); + bool has_password = components.count(component::Password); + if (has_username && has_password) { + m_username = components.at(component::Username); + m_password = components.at(component::Password); + } + else if ((has_username && !has_password) || (!has_username && has_password)) { + throw std::invalid_argument("If a username or password is supplied, both must be provided."); + } + + if (components.count(component::Host)) { + m_host = components.at(component::Host); + } + + if (components.count(component::Port)) { + m_port = std::stoul(components.at(component::Port)); + } + + if (components.count(component::Path)) { + m_path = components.at(component::Path); + } + else { + throw std::invalid_argument("A path is required on a hierarchical URI, even an empty path."); + } + } + else { + if (components.count(component::Username) + || components.count(component::Password) + || components.count(component::Host) + || components.count(component::Port) + || components.count(component::Path)) { + throw std::invalid_argument("None of the hierarchical components are allowed in a non-hierarchical URI."); + } + + if (components.count(component::Content)) { + m_content = components.at(component::Content); + } + else { + throw std::invalid_argument( + "Content is a required component for a non-hierarchical URI, even an empty string." + ); + } + } + + if (components.count(component::Query)) { + m_query = components.at(component::Query); + } + + if (components.count(component::Fragment)) { + m_fragment = components.at(component::Fragment); + } + } + + uri(uri const &other, std::map const &replacements) : + m_category(other.m_category), + m_path_is_rooted(other.m_path_is_rooted), + m_separator(other.m_separator) + { + m_scheme = (replacements.count(component::Scheme)) + ? replacements.at(component::Scheme) : other.m_scheme; + + if (m_category == scheme_category::Hierarchical) { + m_username = (replacements.count(component::Username)) + ? replacements.at(component::Username) : other.m_username; + + m_password = (replacements.count(component::Password)) + ? replacements.at(component::Password) : other.m_password; + + m_host = (replacements.count(component::Host)) + ? replacements.at(component::Host) : other.m_host; + + m_port = (replacements.count(component::Port)) + ? std::stoul(replacements.at(component::Port)) : other.m_port; + + m_path = (replacements.count(component::Path)) + ? replacements.at(component::Path) : other.m_path; + } + else { + m_content = (replacements.count(component::Content)) + ? replacements.at(component::Content) : other.m_content; + } + + m_query = (replacements.count(component::Query)) + ? replacements.at(component::Query) : other.m_query; + + m_fragment = (replacements.count(component::Fragment)) + ? replacements.at(component::Fragment) : other.m_fragment; + } + + // Copy constructor; just use the copy assignment operator internally. + uri(uri const &other) + { + *this = other; + }; + + // Copy assignment operator + uri &operator=(uri const &other) + { + if (this != &other) { + m_scheme = other.m_scheme; + m_content = other.m_content; + m_username = other.m_username; + m_password = other.m_password; + m_host = other.m_host; + m_path = other.m_path; + m_query = other.m_query; + m_fragment = other.m_fragment; + m_query_dict = other.m_query_dict; + m_category = other.m_category; + m_port = other.m_port; + m_path_is_rooted = other.m_path_is_rooted; + m_separator = other.m_separator; + } + return *this; + } + + ~uri() {}; + + std::string const &get_scheme() const + { + return m_scheme; + }; + + scheme_category get_scheme_category() const + { + return m_category; + }; + + std::string const &get_content() const + { + if (m_category != scheme_category::NonHierarchical) { + throw std::domain_error("The content component is only valid for non-hierarchical URIs."); + } + return m_content; + }; + + std::string const &get_username() const + { + if (m_category != scheme_category::Hierarchical) { + throw std::domain_error("The username component is only valid for hierarchical URIs."); + } + return m_username; + }; + + std::string const &get_password() const + { + if (m_category != scheme_category::Hierarchical) { + throw std::domain_error("The password component is only valid for hierarchical URIs."); + } + return m_password; + }; + + std::string const &get_host() const + { + if (m_category != scheme_category::Hierarchical) { + throw std::domain_error("The host component is only valid for hierarchical URIs."); + } + return m_host; + }; + + unsigned long get_port() const + { + if (m_category != scheme_category::Hierarchical) { + throw std::domain_error("The port component is only valid for hierarchical URIs."); + } + return m_port; + }; + + std::string const &get_path() const + { + if (m_category != scheme_category::Hierarchical) { + throw std::domain_error("The path component is only valid for hierarchical URIs."); + } + return m_path; + }; + + std::string const &get_query() const + { + return m_query; + }; + + std::map const &get_query_dictionary() const + { + return m_query_dict; + }; + + std::string const &get_fragment() const + { + return m_fragment; + }; + + std::string to_string() const + { + std::string full_uri; + full_uri.append(m_scheme); + full_uri.append(":"); + + if (m_content.length() > m_path.length()) { + full_uri.append("//"); + if (!(m_username.empty() || m_password.empty())) { + full_uri.append(m_username); + full_uri.append(":"); + full_uri.append(m_password); + full_uri.append("@"); + } + + full_uri.append(m_host); + + if (m_port != 0) { + full_uri.append(":"); + full_uri.append(std::to_string(m_port)); + } + } + + if (m_path_is_rooted) { + full_uri.append("/"); + } + full_uri.append(m_path); + + if (!m_query.empty()) { + full_uri.append("?"); + full_uri.append(m_query); + } + + if (!m_fragment.empty()) { + full_uri.append("#"); + full_uri.append(m_fragment); + } + + return full_uri; + }; + +private: + + void setup(std::string const &uri_text, scheme_category category) + { + size_t const uri_length = uri_text.length(); + + if (uri_length == 0) { + throw std::invalid_argument("URIs cannot be of zero length."); + } + + std::string::const_iterator cursor = parse_scheme( + uri_text, + uri_text.begin()); + // After calling parse_scheme, *cursor == ':'; none of the following parsers + // expect a separator character, so we advance the cursor upon calling them. + cursor = parse_content(uri_text, (cursor + 1)); + + if ((cursor != uri_text.end()) && (*cursor == '?')) { + cursor = parse_query(uri_text, (cursor + 1)); + } + + if ((cursor != uri_text.end()) && (*cursor == '#')) { + cursor = parse_fragment(uri_text, (cursor + 1)); + } + + init_query_dictionary(); // If the query string is empty, this will be empty too. + + }; + + std::string::const_iterator parse_scheme( + std::string const &uri_text, + std::string::const_iterator scheme_start + ) + { + std::string::const_iterator scheme_end = scheme_start; + while ((scheme_end != uri_text.end()) && (*scheme_end != ':')) { + if (!(std::isalnum(*scheme_end) || (*scheme_end == '-') + || (*scheme_end == '+') || (*scheme_end == '.'))) { + throw std::invalid_argument( + "Invalid character found in the scheme component. Supplied URI was: \"" + + uri_text + "\"." + ); + } + ++scheme_end; + } + + if (scheme_end == uri_text.end()) { + throw std::invalid_argument( + "End of URI found while parsing the scheme. Supplied URI was: \"" + + uri_text + "\"." + ); + } + + if (scheme_start == scheme_end) { + throw std::invalid_argument( + "Scheme component cannot be zero-length. Supplied URI was: \"" + + uri_text + "\"." + ); + } + + m_scheme = std::move(std::string(scheme_start, scheme_end)); + return scheme_end; + }; + + std::string::const_iterator parse_content( + std::string const &uri_text, + std::string::const_iterator content_start + ) + { + std::string::const_iterator content_end = content_start; + while ((content_end != uri_text.end()) && (*content_end != '?') && (*content_end != '#')) { + ++content_end; + } + + m_content = std::move(std::string(content_start, content_end)); + + if ((m_category == scheme_category::Hierarchical) && (m_content.length() > 0)) { + // If it's a hierarchical URI, the content should be parsed for the hierarchical components. + std::string::const_iterator path_start = m_content.begin(); + std::string::const_iterator path_end = m_content.end(); + if (!m_content.compare(0, 2, "//")) { + // In this case an authority component is present. + std::string::const_iterator authority_cursor = (m_content.begin() + 2); + if (m_content.find_first_of('@') != std::string::npos) { + std::string::const_iterator userpass_divider = parse_username( + uri_text, + m_content, + authority_cursor + ); + authority_cursor = parse_password(uri_text, m_content, (userpass_divider + 1)); + // After this call, *authority_cursor == '@', so we skip over it. + ++authority_cursor; + } + + authority_cursor = parse_host(uri_text, m_content, authority_cursor); + + if ((authority_cursor != m_content.end()) && (*authority_cursor == ':')) { + authority_cursor = parse_port(uri_text, m_content, (authority_cursor + 1)); + } + + if ((authority_cursor != m_content.end()) && (*authority_cursor == '/')) { + // Then the path is rooted, and we should note this. + m_path_is_rooted = true; + path_start = authority_cursor + 1; + } + + // If we've reached the end and no path is present then set path_start + // to the end. + if (authority_cursor == m_content.end()) { + path_start = m_content.end(); + } + } + else if (!m_content.compare(0, 1, "/")) { + m_path_is_rooted = true; + ++path_start; + } + + // We can now build the path based on what remains in the content string, + // since that's all that exists after the host and optional port component. + m_path = std::move(std::string(path_start, path_end)); + } + return content_end; + }; + + std::string::const_iterator parse_username( + std::string const &uri_text, + std::string const &content, + std::string::const_iterator username_start + ) + { + std::string::const_iterator username_end = username_start; + // Since this is only reachable when '@' was in the content string, we can + // ignore the end-of-string case. + while (*username_end != ':') { + if (*username_end == '@') { + throw std::invalid_argument( + "Username must be followed by a password. Supplied URI was: \"" + + uri_text + "\"." + ); + } + ++username_end; + } + m_username = std::move(std::string(username_start, username_end)); + return username_end; + }; + + std::string::const_iterator parse_password( + std::string const &uri_text, + std::string const &content, + std::string::const_iterator password_start + ) + { + std::string::const_iterator password_end = password_start; + while (*password_end != '@') { + ++password_end; + } + + m_password = std::move(std::string(password_start, password_end)); + return password_end; + }; + + std::string::const_iterator parse_host( + std::string const &uri_text, + std::string const &content, + std::string::const_iterator host_start + ) + { + std::string::const_iterator host_end = host_start; + // So, the host can contain a few things. It can be a domain, it can be an + // IPv4 address, it can be an IPv6 address, or an IPvFuture literal. In the + // case of those last two, it's of the form [...] where what's between the + // brackets is a matter of which IPv?? version it is. + while (host_end != content.end()) { + if (*host_end == '[') { + // We're parsing an IPv6 or IPvFuture address, so we should handle that + // instead of the normal procedure. + while ((host_end != content.end()) && (*host_end != ']')) { + ++host_end; + } + + if (host_end == content.end()) { + throw std::invalid_argument( + "End of content component encountered " + "while parsing the host component. " + "Supplied URI was: \"" + + uri_text + "\"." + ); + } + + ++host_end; + break; + // We can stop looping, we found the end of the IP literal, which is the + // whole of the host component when one's in use. + } + else if ((*host_end == ':') || (*host_end == '/')) { + break; + } + else { + ++host_end; + } + } + + m_host = std::move(std::string(host_start, host_end)); + return host_end; + }; + + std::string::const_iterator parse_port( + std::string const &uri_text, + std::string const &content, + std::string::const_iterator port_start + ) + { + std::string::const_iterator port_end = port_start; + while ((port_end != content.end()) && (*port_end != '/')) { + if (!std::isdigit(*port_end)) { + throw std::invalid_argument( + "Invalid character while parsing the port. " + "Supplied URI was: \"" + uri_text + "\"." + ); + } + + ++port_end; + } + + m_port = std::stoul(std::string(port_start, port_end)); + return port_end; + }; + + std::string::const_iterator parse_query( + std::string const &uri_text, + std::string::const_iterator query_start + ) + { + std::string::const_iterator query_end = query_start; + while ((query_end != uri_text.end()) && (*query_end != '#')) { + // Queries can contain almost any character except hash, which is reserved + // for the start of the fragment. + ++query_end; + } + m_query = std::move(std::string(query_start, query_end)); + return query_end; + }; + + std::string::const_iterator parse_fragment( + std::string const &uri_text, + std::string::const_iterator fragment_start + ) + { + m_fragment = std::move(std::string(fragment_start, uri_text.end())); + return uri_text.end(); + }; + + void init_query_dictionary() + { + if (!m_query.empty()) { + // Loop over the query string looking for '&'s, then check each one for + // an '=' to find keys and values; if there's not an '=' then the key + // will have an empty value in the map. + char separator = (m_separator == query_argument_separator::ampersand) ? '&' : ';'; + size_t carat = 0; + size_t stanza_end = m_query.find_first_of(separator); + do { + std::string stanza = m_query.substr( + carat, + ((stanza_end != std::string::npos) ? (stanza_end - carat) : std::string::npos)); + size_t key_value_divider = stanza.find_first_of('='); + std::string key = stanza.substr(0, key_value_divider); + std::string value; + if (key_value_divider != std::string::npos) { + value = stanza.substr((key_value_divider + 1)); + } + + if (m_query_dict.count(key) != 0) { + throw std::invalid_argument("Bad key in the query string!"); + } + + m_query_dict.emplace(key, value); + carat = ((stanza_end != std::string::npos) ? (stanza_end + 1) + : std::string::npos); + stanza_end = m_query.find_first_of(separator, carat); + } while ((stanza_end != std::string::npos) + || (carat != std::string::npos)); + } + } + + std::string m_scheme; + std::string m_content; + std::string m_username; + std::string m_password; + std::string m_host; + std::string m_path; + std::string m_query; + std::string m_fragment; + + std::map m_query_dict; + + scheme_category m_category; + unsigned long m_port; + bool m_path_is_rooted; + query_argument_separator m_separator; +}; diff --git a/common/repositories/base/base_tool_game_objects_repository.h b/common/repositories/base/base_tool_game_objects_repository.h new file mode 100644 index 000000000..3e1f8600f --- /dev/null +++ b/common/repositories/base/base_tool_game_objects_repository.h @@ -0,0 +1,346 @@ +/** + * DO NOT MODIFY THIS FILE + * + * This repository was automatically generated and is NOT to be modified directly. + * Any repository modifications are meant to be made to the repository extending the base. + * Any modifications to base repositories are to be made by the generator only + * + * @generator ./utils/scripts/generators/repository-generator.pl + * @docs https://eqemu.gitbook.io/server/in-development/developer-area/repositories + */ + +#ifndef EQEMU_BASE_TOOL_GAME_OBJECTS_REPOSITORY_H +#define EQEMU_BASE_TOOL_GAME_OBJECTS_REPOSITORY_H + +#include "../../database.h" +#include "../../string_util.h" +#include + +class BaseToolGameObjectsRepository { +public: + struct ToolGameObjects { + int id; + int zoneid; + std::string zonesn; + std::string object_name; + std::string file_from; + int is_global; + }; + + static std::string PrimaryKey() + { + return std::string("id"); + } + + static std::vector Columns() + { + return { + "id", + "zoneid", + "zonesn", + "object_name", + "file_from", + "is_global", + }; + } + + static std::vector SelectColumns() + { + return { + "id", + "zoneid", + "zonesn", + "object_name", + "file_from", + "is_global", + }; + } + + static std::string ColumnsRaw() + { + return std::string(implode(", ", Columns())); + } + + static std::string SelectColumnsRaw() + { + return std::string(implode(", ", SelectColumns())); + } + + static std::string TableName() + { + return std::string("tool_game_objects"); + } + + static std::string BaseSelect() + { + return fmt::format( + "SELECT {} FROM {}", + SelectColumnsRaw(), + TableName() + ); + } + + static std::string BaseInsert() + { + return fmt::format( + "INSERT INTO {} ({}) ", + TableName(), + ColumnsRaw() + ); + } + + static ToolGameObjects NewEntity() + { + ToolGameObjects entry{}; + + entry.id = 0; + entry.zoneid = 0; + entry.zonesn = ""; + entry.object_name = ""; + entry.file_from = ""; + entry.is_global = 0; + + return entry; + } + + static ToolGameObjects GetToolGameObjectsEntry( + const std::vector &tool_game_objectss, + int tool_game_objects_id + ) + { + for (auto &tool_game_objects : tool_game_objectss) { + if (tool_game_objects.id == tool_game_objects_id) { + return tool_game_objects; + } + } + + return NewEntity(); + } + + static ToolGameObjects FindOne( + Database& db, + int tool_game_objects_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE id = {} LIMIT 1", + BaseSelect(), + tool_game_objects_id + ) + ); + + auto row = results.begin(); + if (results.RowCount() == 1) { + ToolGameObjects entry{}; + + entry.id = atoi(row[0]); + entry.zoneid = atoi(row[1]); + entry.zonesn = row[2] ? row[2] : ""; + entry.object_name = row[3] ? row[3] : ""; + entry.file_from = row[4] ? row[4] : ""; + entry.is_global = atoi(row[5]); + + return entry; + } + + return NewEntity(); + } + + static int DeleteOne( + Database& db, + int tool_game_objects_id + ) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {} = {}", + TableName(), + PrimaryKey(), + tool_game_objects_id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int UpdateOne( + Database& db, + ToolGameObjects tool_game_objects_entry + ) + { + std::vector update_values; + + auto columns = Columns(); + + update_values.push_back(columns[1] + " = " + std::to_string(tool_game_objects_entry.zoneid)); + update_values.push_back(columns[2] + " = '" + EscapeString(tool_game_objects_entry.zonesn) + "'"); + update_values.push_back(columns[3] + " = '" + EscapeString(tool_game_objects_entry.object_name) + "'"); + update_values.push_back(columns[4] + " = '" + EscapeString(tool_game_objects_entry.file_from) + "'"); + update_values.push_back(columns[5] + " = " + std::to_string(tool_game_objects_entry.is_global)); + + auto results = db.QueryDatabase( + fmt::format( + "UPDATE {} SET {} WHERE {} = {}", + TableName(), + implode(", ", update_values), + PrimaryKey(), + tool_game_objects_entry.id + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static ToolGameObjects InsertOne( + Database& db, + ToolGameObjects tool_game_objects_entry + ) + { + std::vector insert_values; + + insert_values.push_back(std::to_string(tool_game_objects_entry.id)); + insert_values.push_back(std::to_string(tool_game_objects_entry.zoneid)); + insert_values.push_back("'" + EscapeString(tool_game_objects_entry.zonesn) + "'"); + insert_values.push_back("'" + EscapeString(tool_game_objects_entry.object_name) + "'"); + insert_values.push_back("'" + EscapeString(tool_game_objects_entry.file_from) + "'"); + insert_values.push_back(std::to_string(tool_game_objects_entry.is_global)); + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES ({})", + BaseInsert(), + implode(",", insert_values) + ) + ); + + if (results.Success()) { + tool_game_objects_entry.id = results.LastInsertedID(); + return tool_game_objects_entry; + } + + tool_game_objects_entry = NewEntity(); + + return tool_game_objects_entry; + } + + static int InsertMany( + Database& db, + std::vector tool_game_objects_entries + ) + { + std::vector insert_chunks; + + for (auto &tool_game_objects_entry: tool_game_objects_entries) { + std::vector insert_values; + + insert_values.push_back(std::to_string(tool_game_objects_entry.id)); + insert_values.push_back(std::to_string(tool_game_objects_entry.zoneid)); + insert_values.push_back("'" + EscapeString(tool_game_objects_entry.zonesn) + "'"); + insert_values.push_back("'" + EscapeString(tool_game_objects_entry.object_name) + "'"); + insert_values.push_back("'" + EscapeString(tool_game_objects_entry.file_from) + "'"); + insert_values.push_back(std::to_string(tool_game_objects_entry.is_global)); + + insert_chunks.push_back("(" + implode(",", insert_values) + ")"); + } + + std::vector insert_values; + + auto results = db.QueryDatabase( + fmt::format( + "{} VALUES {}", + BaseInsert(), + implode(",", insert_chunks) + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static std::vector All(Database& db) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{}", + BaseSelect() + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + ToolGameObjects entry{}; + + entry.id = atoi(row[0]); + entry.zoneid = atoi(row[1]); + entry.zonesn = row[2] ? row[2] : ""; + entry.object_name = row[3] ? row[3] : ""; + entry.file_from = row[4] ? row[4] : ""; + entry.is_global = atoi(row[5]); + + all_entries.push_back(entry); + } + + return all_entries; + } + + static std::vector GetWhere(Database& db, std::string where_filter) + { + std::vector all_entries; + + auto results = db.QueryDatabase( + fmt::format( + "{} WHERE {}", + BaseSelect(), + where_filter + ) + ); + + all_entries.reserve(results.RowCount()); + + for (auto row = results.begin(); row != results.end(); ++row) { + ToolGameObjects entry{}; + + entry.id = atoi(row[0]); + entry.zoneid = atoi(row[1]); + entry.zonesn = row[2] ? row[2] : ""; + entry.object_name = row[3] ? row[3] : ""; + entry.file_from = row[4] ? row[4] : ""; + entry.is_global = atoi(row[5]); + + all_entries.push_back(entry); + } + + return all_entries; + } + + static int DeleteWhere(Database& db, std::string where_filter) + { + auto results = db.QueryDatabase( + fmt::format( + "DELETE FROM {} WHERE {}", + TableName(), + where_filter + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + + static int Truncate(Database& db) + { + auto results = db.QueryDatabase( + fmt::format( + "TRUNCATE TABLE {}", + TableName() + ) + ); + + return (results.Success() ? results.RowsAffected() : 0); + } + +}; + +#endif //EQEMU_BASE_TOOL_GAME_OBJECTS_REPOSITORY_H diff --git a/common/repositories/tool_game_objects_repository.h b/common/repositories/tool_game_objects_repository.h new file mode 100644 index 000000000..d4d6009b5 --- /dev/null +++ b/common/repositories/tool_game_objects_repository.h @@ -0,0 +1,70 @@ +/** + * EQEmulator: Everquest Server Emulator + * Copyright (C) 2001-2020 EQEmulator Development Team (https://github.com/EQEmu/Server) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY except by those people which sell it, which + * are required to give you total support for your newly bought product; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef EQEMU_TOOL_GAME_OBJECTS_REPOSITORY_H +#define EQEMU_TOOL_GAME_OBJECTS_REPOSITORY_H + +#include "../database.h" +#include "../string_util.h" +#include "base/base_tool_game_objects_repository.h" + +class ToolGameObjectsRepository: public BaseToolGameObjectsRepository { +public: + + /** + * This file was auto generated and can be modified and extended upon + * + * Base repository methods are automatically + * generated in the "base" version of this repository. The base repository + * is immutable and to be left untouched, while methods in this class + * are used as extension methods for more specific persistence-layer + * accessors or mutators. + * + * Base Methods (Subject to be expanded upon in time) + * + * Note: Not all tables are designed appropriately to fit functionality with all base methods + * + * InsertOne + * UpdateOne + * DeleteOne + * FindOne + * GetWhere(std::string where_filter) + * DeleteWhere(std::string where_filter) + * InsertMany + * All + * + * Example custom methods in a repository + * + * ToolGameObjectsRepository::GetByZoneAndVersion(int zone_id, int zone_version) + * ToolGameObjectsRepository::GetWhereNeverExpires() + * ToolGameObjectsRepository::GetWhereXAndY() + * ToolGameObjectsRepository::DeleteWhereXAndY() + * + * Most of the above could be covered by base methods, but if you as a developer + * find yourself re-using logic for other parts of the code, its best to just make a + * method that can be re-used easily elsewhere especially if it can use a base repository + * method and encapsulate filters there + */ + + // Custom extended repository methods here + +}; + +#endif //EQEMU_TOOL_GAME_OBJECTS_REPOSITORY_H diff --git a/utils/scripts/generators/repository-generator.pl b/utils/scripts/generators/repository-generator.pl index b387a7d63..bd7514a93 100644 --- a/utils/scripts/generators/repository-generator.pl +++ b/utils/scripts/generators/repository-generator.pl @@ -157,7 +157,7 @@ foreach my $table_to_generate (@tables) { $table_found_in_schema = 0; } - if ($table_found_in_schema == 0) { + if ($table_found_in_schema == 0 && ($requested_table_to_generate eq "" || $requested_table_to_generate eq "all")) { print "Table [$table_to_generate] not found in schema, skipping\n"; next; } diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index 184b7d318..9f519b498 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -80,6 +80,7 @@ SET(zone_sources fearpath.cpp forage.cpp global_loot_manager.cpp + gm_commands/door_manipulation.cpp groups.cpp guild.cpp guild_mgr.cpp @@ -198,6 +199,7 @@ SET(zone_headers fastmath.h forage.h global_loot_manager.h + gm_commands/door_manipulation.h groups.h guild_mgr.h hate_list.h diff --git a/zone/client.cpp b/zone/client.cpp index 7584a0bd1..2ad737405 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -10515,3 +10515,13 @@ void Client::ApplyWeaponsStance() weaponstance.spellbonus_enabled = false; } } + +uint16 Client::GetDoorToolEntityId() const +{ + return m_door_tool_entity_id; +} + +void Client::SetDoorToolEntityId(uint16 door_tool_entity_id) +{ + Client::m_door_tool_entity_id = door_tool_entity_id; +} diff --git a/zone/client.h b/zone/client.h index cb39637a1..966d7976c 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1764,9 +1764,17 @@ private: int Haste; //precalced value uint32 tmSitting; // time stamp started sitting, used for HP regen bonus added on MAY 5, 2004 + + // dev tools bool display_mob_info_window; bool dev_tools_enabled; + uint16 m_door_tool_entity_id; +public: + uint16 GetDoorToolEntityId() const; + void SetDoorToolEntityId(uint16 door_tool_entity_id); +private: + int32 max_end; int32 current_endurance; diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index dd22111fa..0a05d7580 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -66,6 +66,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/repositories/character_instance_safereturns_repository.h" #include "../common/repositories/criteria/content_filter_criteria.h" #include "../common/shared_tasks.h" +#include "gm_commands/door_manipulation.h" #ifdef BOTS #include "bot.h" @@ -4356,6 +4357,20 @@ void Client::Handle_OP_ClickDoor(const EQApplicationPacket *app) return; } + // set door selected + if (IsDevToolsEnabled()) { + SetDoorToolEntityId(currentdoor->GetEntityID()); + DoorManipulation::CommandHeader(this); + Message( + Chat::White, + fmt::format( + "Door ({}) [{}]", + currentdoor->GetEntityID(), + EQ::SayLinkEngine::GenerateQuestSaylink("#door edit", false, "#door edit") + ).c_str() + ); + } + char buf[20]; snprintf(buf, 19, "%u", cd->doorid); buf[19] = '\0'; diff --git a/zone/command.cpp b/zone/command.cpp index 4c9bcc0bb..4db232d80 100755 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -78,6 +78,7 @@ #include "../common/content/world_content_service.h" #include "../common/http/httplib.h" #include "../common/shared_tasks.h" +#include "gm_commands/door_manipulation.h" extern QueryServ* QServ; extern WorldServer worldserver; @@ -202,6 +203,7 @@ int command_init(void) command_add("disablerecipe", "[recipe_id] - Disables a recipe using the recipe id.", 80, command_disablerecipe) || command_add("disarmtrap", "Analog for ldon disarm trap for the newer clients since we still don't have it working.", 80, command_disarmtrap) || command_add("distance", "- Reports the distance between you and your target.", 80, command_distance) || + command_add("door", "Door editing command", 80, command_door) || command_add("doanim", "[animnum] [type] - Send an EmoteAnim for you or your target", 50, command_doanim) || command_add("dz", "Manage expeditions and dynamic zone instances", 80, command_dz) || command_add("dzkickplayers", "Removes all players from current expedition. (/kickplayers alternative for pre-RoF clients)", 0, command_dzkickplayers) || @@ -12940,6 +12942,10 @@ void command_distance(Client *c, const Seperator *sep) { } } +void command_door(Client *c, const Seperator *sep) { + DoorManipulation::CommandHandler(c, sep); +} + void command_cvs(Client *c, const Seperator *sep) { if(c) diff --git a/zone/command.h b/zone/command.h index cb3f5ea04..9c2aef51b 100644 --- a/zone/command.h +++ b/zone/command.h @@ -90,6 +90,7 @@ void command_devtools(Client *c, const Seperator *sep); void command_details(Client *c, const Seperator *sep); void command_disablerecipe(Client *c, const Seperator *sep); void command_disarmtrap(Client *c, const Seperator *sep); +void command_door(Client *c, const Seperator *sep); void command_distance(Client *c, const Seperator *sep); void command_doanim(Client *c, const Seperator *sep); void command_dz(Client *c, const Seperator *sep); diff --git a/zone/doors.cpp b/zone/doors.cpp index ba3d36d21..2458e7f69 100644 --- a/zone/doors.cpp +++ b/zone/doors.cpp @@ -801,6 +801,12 @@ void Doors::SetIncline(int in) { entity_list.RespawnAllDoors(); } +void Doors::SetInvertState(int in) { + entity_list.DespawnAllDoors(); + invert_state = in; + entity_list.RespawnAllDoors(); +} + void Doors::SetOpenType(uint8 in) { entity_list.DespawnAllDoors(); open_type = in; diff --git a/zone/doors.h b/zone/doors.h index d7fbfc021..455dfd03f 100644 --- a/zone/doors.h +++ b/zone/doors.h @@ -54,6 +54,7 @@ public: void SetDoorName(const char *name); void SetEntityID(uint32 entity) { entity_id = entity; } void SetIncline(int in); + void SetInvertState(int in); void SetKeyItem(uint32 in) { key_item_id = in; } void SetLocation(float x, float y, float z); void SetLockpick(uint16 in) { lockpick = in; } diff --git a/zone/gm_commands/door_manipulation.cpp b/zone/gm_commands/door_manipulation.cpp new file mode 100644 index 000000000..c9a950e52 --- /dev/null +++ b/zone/gm_commands/door_manipulation.cpp @@ -0,0 +1,779 @@ +#include "door_manipulation.h" +#include "../doors.h" +#include "../../common/misc_functions.h" + +#define MAX_CLIENT_MESSAGE_LENGTH 2000 + +void DoorManipulation::CommandHandler(Client *c, const Seperator *sep) +{ + // this should never happen + if (!c) { + return; + } + + // args + std::string arg1(sep->arg[1]); + std::string arg2(sep->arg[2]); + std::string arg3(sep->arg[3]); + + // table check + std::string table_name = "tool_game_objects"; + std::string url = "https://raw.githubusercontent.com/EQEmu/database-tool-sqls/main/tool_game_objects.sql"; + if (!database.DoesTableExist(table_name)) { + c->Message( + Chat::White, + fmt::format( + "Table [{}] does not exist. Downloading from [{}] and installing locally", + table_name, + url + ).c_str() + ); + database.SourceDatabaseTableFromUrl( + table_name, + url + ); + } + + // option + if (arg1.empty()) { + DoorManipulation::CommandHeader(c); + c->Message(Chat::White, "#door create | Creates a door from a model. (Example IT78 creates a campfire)"); + c->Message(Chat::White, "#door setinvertstate [0|1] | Sets selected door invert state"); + c->Message(Chat::White, "#door setincline | Sets selected door incline"); + c->Message(Chat::White, "#door opentype | Sets selected door opentype"); + c->Message( + Chat::White, + fmt::format( + "#door model | Changes door model for selected door or select from [{}] or [{}]", + EQ::SayLinkEngine::GenerateQuestSaylink("#door showmodelszone", false, "local zone"), + EQ::SayLinkEngine::GenerateQuestSaylink("#door showmodelsglobal", false, "global") + ).c_str() + ); + c->Message( + Chat::White, + "#door showmodelsfromfile | Shows models from s3d or eqg file. Example tssequip.eqg or wallet01.eqg" + ); + + c->Message( + Chat::White, + fmt::format( + "{} | Shows available models in the current zone that you are in", + EQ::SayLinkEngine::GenerateQuestSaylink("#door showmodelszone", false, "#door showmodelszone") + ).c_str() + ); + + c->Message( + Chat::White, + fmt::format( + "{} | Shows available models globally by first listing all global model files", + EQ::SayLinkEngine::GenerateQuestSaylink("#door showmodelsglobal", false, "#door showmodelsglobal") + ).c_str() + ); + + c->Message(Chat::White, "#door save | Creates database entry for selected door"); + c->Message( + Chat::White, + fmt::format( + "{} - Brings up editing interface for selected door", + EQ::SayLinkEngine::GenerateQuestSaylink("#door edit", false, "#door edit") + ).c_str() + ); + c->Message( + Chat::White, + fmt::format( + "{} - lists doors in zone", + EQ::SayLinkEngine::GenerateQuestSaylink("#list doors", false, "#list doors") + ).c_str() + ); + + return; + } + + // edit menu + if (arg1 == "edit") { + Doors *door = entity_list.GetDoorsByID(c->GetDoorToolEntityId()); + if (door) { + c->Message( + Chat::White, + fmt::format( + "Door Selected ID [{}] Name [{}] OpenType [{}] Invertstate [{} | {}/{}] ", + c->GetDoorToolEntityId(), + door->GetDoorName(), + door->GetOpenType(), + door->GetInvertState(), + EQ::SayLinkEngine::GenerateQuestSaylink("#door setinvertstate 0", false, "0"), + EQ::SayLinkEngine::GenerateQuestSaylink("#door setinvertstate 1", false, "1") + ).c_str() + ); + + const std::string move_x_action = "move_x"; + const std::string move_y_action = "move_y"; + const std::string move_z_action = "move_z"; + const std::string move_h_action = "move_h"; + const std::string set_size_action = "set_size"; + + std::vector move_options = { + move_x_action, + move_y_action, + move_z_action, + move_h_action, + set_size_action + }; + std::vector move_x_options_positive; + std::vector move_x_options_negative; + std::vector move_y_options_positive; + std::vector move_y_options_negative; + std::vector move_z_options_positive; + std::vector move_z_options_negative; + std::vector move_h_options_positive; + std::vector move_h_options_negative; + std::vector set_size_options_positive; + std::vector set_size_options_negative; + for (const auto &move_option : move_options) { + if (move_option == move_x_action) { + move_x_options_positive.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} .25", move_option), + false, + ".25" + ) + ); + + for (int move_index = 0; move_index <= 15; move_index += 5) { + int value = (move_index == 0 ? 1 : move_index); + move_x_options_positive.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} {}", move_option, value), + false, + fmt::format("{}", std::abs(value)) + ) + ); + } + + for (int move_index = -15; move_index <= 0; move_index += 5) { + int value = (move_index == 0 ? 1 : move_index); + move_x_options_negative.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} {}", move_option, value), + false, + fmt::format("{}", std::abs(value)) + ) + ); + } + + move_x_options_negative.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} -.25", move_option), + false, + "-.25" + ) + ); + } + else if (move_option == move_y_action) { + move_y_options_positive.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} .25", move_option), + false, + ".25" + ) + ); + + for (int move_index = 0; move_index <= 15; move_index += 5) { + int value = (move_index == 0 ? 1 : move_index); + move_y_options_positive.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} {}", move_option, value), + false, + fmt::format("{}", std::abs(value)) + ) + ); + } + + for (int move_index = -15; move_index <= 0; move_index += 5) { + int value = (move_index == 0 ? 1 : move_index); + move_y_options_negative.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} {}", move_option, value), + false, + fmt::format("{}", std::abs(value)) + ) + ); + } + + move_y_options_negative.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} -.25", move_option), + false, + "-.25" + ) + ); + } + else if (move_option == move_z_action) { + move_z_options_positive.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} .25", move_option), + false, + ".25" + ) + ); + + for (int move_index = 0; move_index <= 15; move_index += 5) { + int value = (move_index == 0 ? 1 : move_index); + move_z_options_positive.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} {}", move_option, value), + false, + fmt::format("{}", std::abs(value)) + ) + ); + } + + for (int move_index = -15; move_index <= 0; move_index += 5) { + int value = (move_index == 0 ? 1 : move_index); + move_z_options_negative.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} {}", move_option, value), + false, + fmt::format("{}", std::abs(value)) + ) + ); + } + + move_z_options_negative.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} -.25", move_option), + false, + "-.25" + ) + ); + } + else if (move_option == move_h_action) { + for (int move_index = 0; move_index <= 50; move_index += 5) { + int value = (move_index == 0 ? 1 : move_index); + move_h_options_positive.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} {}", move_option, value), + false, + fmt::format("{}", std::abs(value)) + ) + ); + } + + for (int move_index = -50; move_index <= 0; move_index += 5) { + int value = (move_index == 0 ? 1 : move_index); + move_h_options_negative.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} {}", move_option, value), + false, + fmt::format("{}", std::abs(value)) + ) + ); + } + } + else if (move_option == set_size_action) { + for (int move_index = 0; move_index <= 100; move_index += 10) { + int value = (move_index == 0 ? 1 : move_index); + set_size_options_positive.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} {}", move_option, value), + false, + fmt::format("{}", std::abs(value)) + ) + ); + } + + for (int move_index = -100; move_index <= 0; move_index += 10) { + int value = (move_index == 0 ? 1 : move_index); + set_size_options_negative.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door edit {} {}", move_option, value), + false, + fmt::format("{}", std::abs(value)) + ) + ); + } + } + } + + // we're passing a move action here + if (!arg3.empty() && StringIsNumber(arg3)) { + int x_move = 0; + int y_move = 0; + int z_move = 0; + int h_move = 0; + int set_size = 0; + + if (arg2 == move_x_action) { + x_move = std::atoi(arg3.c_str()); + } + if (arg2 == move_y_action) { + y_move = std::atoi(arg3.c_str()); + } + if (arg2 == move_z_action) { + z_move = std::atoi(arg3.c_str()); + } + if (arg2 == move_h_action) { + h_move = std::atoi(arg3.c_str()); + } + if (arg2 == set_size_action) { + set_size = std::atoi(arg3.c_str()); + } + + door->SetLocation( + door->GetX() + x_move, + door->GetY() + y_move, + door->GetZ() + z_move + ); + + glm::vec4 door_position = door->GetPosition(); + door_position.w = door_position.w + h_move; + door->SetPosition(door_position); + door->SetSize(door->GetSize() + set_size); + } + + // spawn and move helpers + uint16 helper_mob_x_negative = 0; + uint16 helper_mob_x_positive = 0; + uint16 helper_mob_y_positive = 0; + uint16 helper_mob_y_negative = 0; + + for (auto &n: entity_list.GetNPCList()) { + NPC *npc = n.second; + std::string npc_name = npc->GetName(); + if (npc_name.find("-X") != std::string::npos) { + helper_mob_x_negative = npc->GetID(); + } + if (npc_name.find("-Y") != std::string::npos) { + helper_mob_y_negative = npc->GetID(); + } + if (npc_name.find("+X") != std::string::npos) { + helper_mob_x_positive = npc->GetID(); + } + if (npc_name.find("+Y") != std::string::npos) { + helper_mob_y_positive = npc->GetID(); + } + } + + // -X + glm::vec4 door_position = door->GetPosition(); + if (helper_mob_x_negative == 0) { + door_position.x = door_position.x - 15; + helper_mob_x_negative = NPC::SpawnNodeNPC("-X", "", door_position)->GetID(); + } + else { + auto n = entity_list.GetNPCByID(helper_mob_x_negative); + n->GMMove(door->GetX() - 15, door->GetY(), door->GetZ(), n->GetHeading()); + } + + // +X + door_position = door->GetPosition(); + if (helper_mob_x_positive == 0) { + door_position.x = door_position.x + 15; + helper_mob_x_positive = NPC::SpawnNodeNPC("+X", "", door_position)->GetID(); + } + else { + auto n = entity_list.GetNPCByID(helper_mob_x_positive); + n->GMMove(door->GetX() + 15, door->GetY(), door->GetZ(), n->GetHeading()); + } + + // -Y + door_position = door->GetPosition(); + if (helper_mob_y_negative == 0) { + door_position.y = door_position.y - 15; + helper_mob_y_negative = NPC::SpawnNodeNPC("-Y", "", door_position)->GetID(); + } + else { + auto n = entity_list.GetNPCByID(helper_mob_y_negative); + n->GMMove(door->GetX(), door->GetY() - 15, door->GetZ(), n->GetHeading()); + } + + // +Y + door_position = door->GetPosition(); + if (helper_mob_y_positive == 0) { + door_position.y = door_position.y + 15; + helper_mob_y_positive = NPC::SpawnNodeNPC("+Y", "", door_position)->GetID(); + } + else { + auto n = entity_list.GetNPCByID(helper_mob_y_positive); + n->GMMove(door->GetX(), door->GetY() + 15, door->GetZ(), n->GetHeading()); + } + + c->Message( + Chat::White, + fmt::format( + "Name [{}] [{}] [{}] [{}]", + door->GetDoorName(), + EQ::SayLinkEngine::GenerateQuestSaylink( + "#door save", + false, + "Save" + ), + EQ::SayLinkEngine::GenerateQuestSaylink( + "#door changemodelqueue", + false, + "Change Model" + ), + EQ::SayLinkEngine::GenerateQuestSaylink( + "#door setinclineinc", + false, + "Incline" + ) + ).c_str() + ); + c->Message( + Chat::White, + fmt::format( + "[{}] - [X] + [{}]", + implode(" | ", move_x_options_negative), + implode(" | ", move_x_options_positive) + ).c_str() + ); + c->Message( + Chat::White, + fmt::format( + "[{}] - [Y] + [{}]", + implode(" | ", move_y_options_negative), + implode(" | ", move_y_options_positive) + ).c_str() + ); + c->Message( + Chat::White, + fmt::format( + "[{}] - [Z] + [{}]", + implode(" | ", move_z_options_negative), + implode(" | ", move_z_options_positive) + ).c_str() + ); + c->Message( + Chat::White, + fmt::format( + "[{}] - [H] + [{}]", + implode(" | ", move_h_options_negative), + implode(" | ", move_h_options_positive) + ).c_str() + ); + c->Message( + Chat::White, + fmt::format( + "[{}] - [Size] + [{}]", + implode(" | ", set_size_options_negative), + implode(" | ", set_size_options_positive) + ).c_str() + ); + + return; + } + + c->Message(Chat::Red, "Door selection invalid..."); + } + + // create + if (arg1 == "create") { + std::string model = str_toupper(arg2); + uint16 entity_id = entity_list.CreateDoor( + model.c_str(), + c->GetPosition(), + 58, + 100 + ); + + c->Message( + Chat::White, + fmt::format("Creating door entity_id [{}] with model [{}]", entity_id, model).c_str()); + c->SetDoorToolEntityId(entity_id); + } + + // set model + if (arg1 == "model") { + Doors *door = entity_list.GetDoorsByID(c->GetDoorToolEntityId()); + std::string model = str_toupper(arg2); + if (door) { + door->SetDoorName(model.c_str()); + } + } + + // change model queue + if (arg1 == "changemodelqueue") { + c->Message( + Chat::White, + fmt::format( + "#door model | Changes door model for selected door or select from [{}] or [{}]", + EQ::SayLinkEngine::GenerateQuestSaylink("#door showmodelszone", false, "local zone"), + EQ::SayLinkEngine::GenerateQuestSaylink("#door showmodelsglobal", false, "global") + ).c_str() + ); + } + + // open type + if (arg1 == "opentype" && !arg2.empty() && StringIsNumber(arg2)) { + Doors *door = entity_list.GetDoorsByID(c->GetDoorToolEntityId()); + if (door) { + door->SetOpenType(std::atoi(arg2.c_str())); + } + } + + // incline + if (arg1 == "setincline" && !arg2.empty() && StringIsNumber(arg2)) { + Doors *door = entity_list.GetDoorsByID(c->GetDoorToolEntityId()); + if (door) { + door->SetIncline(std::atoi(arg2.c_str())); + } + } + + // incline + if (arg1 == "setinvertstate" && !arg2.empty() && StringIsNumber(arg2)) { + Doors *door = entity_list.GetDoorsByID(c->GetDoorToolEntityId()); + if (door) { + door->SetInvertState(std::atoi(arg2.c_str())); + } + } + + // save + if (arg1 == "save") { + Doors *door = entity_list.GetDoorsByID(c->GetDoorToolEntityId()); + if (door) { + door->CreateDatabaseEntry(); + c->Message(Chat::White, "Door saved"); + } + } + + // incline incremental + if (arg1 == "setinclineinc" && !arg2.empty() && StringIsNumber(arg2)) { + Doors *door = entity_list.GetDoorsByID(c->GetDoorToolEntityId()); + if (door) { + door->SetIncline(door->GetIncline() + std::atoi(arg2.c_str())); + } + } + if (arg1 == "setinclineinc") { + std::map incline_values = { + {.01, "Upright"}, + {63.75, "45 Degrees",}, + {130, "90 Degrees"}, + {192.5, "135 Degrees"}, + {255, "180 Degrees"}, + {321.25, "225 Degrees"}, + {385, "270 Degrees"}, + {448.75, "315 Degrees"}, + {512.5, "360 Degrees"} + }; + + std::vector incline_normal_options; + std::vector incline_positive_options; + std::vector incline_negative_options; + for (auto incline_value : incline_values) { + incline_normal_options.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format( + "#door setincline {}", + incline_value.first + ), + false, + incline_value.second + ) + ); + } + + for (int incline_index = 0; incline_index <= 100; incline_index += 10) { + int incline_value = (incline_index == 0 ? 1 : incline_index); + incline_positive_options.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format( + "#door setinclineinc {}", + incline_value + ), + false, + itoa(std::abs(incline_value)) + ) + ); + } + + for (int incline_index = -100; incline_index <= 1; incline_index += 10) { + int incline_value = (incline_index == 0 ? -1 : incline_index); + incline_negative_options.emplace_back( + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format( + "#door setinclineinc {}", + incline_value + ), + false, + itoa(std::abs(incline_value)) + ) + ); + } + + c->Message( + Chat::White, + fmt::format( + "[Incline] [{}]", + implode(" | ", incline_normal_options) + ).c_str() + ); + + c->Message( + Chat::White, + fmt::format( + "[Incline Increments] [{}] - | + [{}]", + implode(" | ", incline_negative_options), + implode(" | ", incline_positive_options) + ).c_str() + ); + } + + // show models in zone + if (arg1 == "showmodelsglobal") { + auto game_objects = ToolGameObjectsRepository::GetWhere( + database, + "object_name LIKE '%IT%' AND zoneid = 0 AND object_name NOT LIKE '%OBJ%' GROUP by file_from" + ); + + if (game_objects.empty()) { + c->Message(Chat::White, "There are no models to display..."); + } + + c->Message(Chat::White, "------------------------------------------------"); + c->Message(Chat::White, "# Models (Global)"); + c->Message(Chat::White, "------------------------------------------------"); + + DisplayModelsFromFileResults(c, game_objects); + } + + // show models in zone + if (arg1 == "showmodelszone") { + auto game_objects = ToolGameObjectsRepository::GetWhere( + database, + fmt::format("zoneid = {}", zone->GetZoneID()) + ); + + if (game_objects.empty()) { + c->Message(Chat::White, "There are no models for this zone..."); + } + + c->Message(Chat::White, "------------------------------------------------"); + c->Message(Chat::White, "# Models from zone"); + c->Message(Chat::White, "------------------------------------------------"); + + DisplayObjectResultToClient(c, game_objects); + } + + // show models from file name + if (arg1 == "showmodelsfromfile" && !arg2.empty()) { + const std::string &file_name = arg2; + auto game_objects = ToolGameObjectsRepository::GetWhere( + database, + fmt::format("file_from = '{}'", file_name) + ); + + if (game_objects.empty()) { + c->Message(Chat::White, "There are no models for this zone..."); + } + + c->Message(Chat::White, "------------------------------------------------"); + c->Message(Chat::White, fmt::format("# Models from file name [{}]", file_name).c_str()); + c->Message(Chat::White, "------------------------------------------------"); + + DisplayObjectResultToClient(c, game_objects); + } +} + +void DoorManipulation::CommandHeader(Client *c) +{ + c->Message(Chat::White, "------------------------------------------------"); + c->Message(Chat::White, "# Door Commands"); + c->Message(Chat::White, "------------------------------------------------"); +} + +void DoorManipulation::DisplayObjectResultToClient( + Client *c, + std::vector game_objects +) +{ + std::vector say_links; + + for (auto &g: game_objects) { + say_links.emplace_back( + fmt::format( + "[{}] ", + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door model {}", g.object_name), + false, + g.object_name + ) + ) + ); + } + + int character_length = 0; + std::vector buffered_links; + + for (auto &links: say_links) { + buffered_links.emplace_back(links); + character_length += links.length(); + + // empty buffer + if (character_length > MAX_CLIENT_MESSAGE_LENGTH) { + std::string message_buffer; + + for (auto &buffered_link: buffered_links) { + message_buffer += buffered_link; + } + + c->Message(Chat::White, message_buffer.c_str()); + + // reset + character_length = 0; + buffered_links = {}; + } + } + + if (!buffered_links.empty()) { + c->Message(Chat::White, implode(" ", buffered_links).c_str()); + } +} + +void DoorManipulation::DisplayModelsFromFileResults( + Client *c, + std::vector game_objects +) +{ + std::vector say_links; + + for (auto &g: game_objects) { + say_links.emplace_back( + fmt::format( + "[{}] ", + EQ::SayLinkEngine::GenerateQuestSaylink( + fmt::format("#door showmodelsfromfile {}", g.file_from), + false, + g.file_from + ) + ) + ); + } + + int character_length = 0; + std::vector buffered_links; + + for (auto &links: say_links) { + buffered_links.emplace_back(links); + character_length += links.length(); + + // empty buffer + if (character_length > MAX_CLIENT_MESSAGE_LENGTH) { + std::string message_buffer; + + for (auto &buffered_link: buffered_links) { + message_buffer += buffered_link; + } + + c->Message(Chat::White, message_buffer.c_str()); + + // reset + character_length = 0; + buffered_links = {}; + } + } + + if (!buffered_links.empty()) { + c->Message(Chat::White, implode(" ", buffered_links).c_str()); + } +} diff --git a/zone/gm_commands/door_manipulation.h b/zone/gm_commands/door_manipulation.h new file mode 100644 index 000000000..d448c3bc7 --- /dev/null +++ b/zone/gm_commands/door_manipulation.h @@ -0,0 +1,23 @@ +#ifndef EQEMU_DOOR_MANIPULATION_H +#define EQEMU_DOOR_MANIPULATION_H + +#include "../client.h" +#include "../../common/repositories/tool_game_objects_repository.h" + +class DoorManipulation { + +public: + static void CommandHandler(Client *c, const Seperator *sep); + static void CommandHeader(Client *c); + static void DisplayObjectResultToClient( + Client *c, + std::vector game_objects + ); + static void DisplayModelsFromFileResults( + Client *c, + std::vector game_objects + ); +}; + + +#endif //EQEMU_DOOR_MANIPULATION_H diff --git a/zone/npc.cpp b/zone/npc.cpp index d7e4aa796..85a843ca0 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -284,7 +284,7 @@ NPC::NPC(const NPCType *npc_type_data, Spawn2 *in_respawn, const glm::vec4 &posi entity_list.MakeNameUnique(name); npc_aggro = npc_type_data->npc_aggro; - + AISpellVar.fail_recast = static_cast(RuleI(Spells, AI_SpellCastFinishedFailRecast)); AISpellVar.engaged_no_sp_recast_min = static_cast(RuleI(Spells, AI_EngagedNoSpellMinRecast)); AISpellVar.engaged_no_sp_recast_max = static_cast(RuleI(Spells, AI_EngagedNoSpellMaxRecast)); @@ -694,7 +694,7 @@ bool NPC::HasItem(uint32 item_id) { if (loot_item->item_id == item_id) { return true; } - } + } return false; } @@ -1124,7 +1124,7 @@ bool NPC::SpawnZoneController() memset(npc_type, 0, sizeof(NPCType)); strncpy(npc_type->name, "zone_controller", 60); - npc_type->current_hp = 2000000000; + npc_type->current_hp = 2000000000; npc_type->max_hp = 2000000000; npc_type->hp_regen = 100000000; npc_type->race = 240; @@ -1207,7 +1207,7 @@ void NPC::SpawnGridNodeNPC(const glm::vec4 &position, int32 grid_id, int32 grid_ entity_list.AddNPC(npc); } -void NPC::SpawnZonePointNodeNPC(std::string name, const glm::vec4 &position) +NPC * NPC::SpawnZonePointNodeNPC(std::string name, const glm::vec4 &position) { auto npc_type = new NPCType; memset(npc_type, 0, sizeof(NPCType)); @@ -1235,6 +1235,8 @@ void NPC::SpawnZonePointNodeNPC(std::string name, const glm::vec4 &position) npc_type->show_name = true; npc_type->findable = true; + strcpy(npc_type->special_abilities, "12,1^13,1^14,1^15,1^16,1^17,1^19,1^22,1^24,1^25,1^28,1^31,1^35,1^39,1^42,1"); + auto node_position = glm::vec4(position.x, position.y, position.z, position.w); auto npc = new NPC(npc_type, nullptr, node_position, GravityBehavior::Flying); @@ -1243,14 +1245,15 @@ void NPC::SpawnZonePointNodeNPC(std::string name, const glm::vec4 &position) npc->GiveNPCTypeData(npc_type); entity_list.AddNPC(npc); + + return npc; } NPC * NPC::SpawnNodeNPC(std::string name, std::string last_name, const glm::vec4 &position) { auto npc_type = new NPCType; memset(npc_type, 0, sizeof(NPCType)); - sprintf(npc_type->name, "%s", name.c_str()); - sprintf(npc_type->lastname, "%s", last_name.c_str()); + strncpy(npc_type->name, name.c_str(), 60); npc_type->current_hp = 4000000; npc_type->max_hp = 4000000; @@ -1272,9 +1275,13 @@ NPC * NPC::SpawnNodeNPC(std::string name, std::string last_name, const glm::vec4 npc_type->findable = true; npc_type->runspeed = 1.25; + strcpy(npc_type->special_abilities, "12,1^13,1^14,1^15,1^16,1^17,1^19,1^22,1^24,1^25,1^28,1^31,1^35,1^39,1^42,1"); + auto node_position = glm::vec4(position.x, position.y, position.z, position.w); auto npc = new NPC(npc_type, nullptr, node_position, GravityBehavior::Flying); + npc->name[strlen(npc->name) - 3] = (char) NULL; + npc->GiveNPCTypeData(npc_type); entity_list.AddNPC(npc, true, true); diff --git a/zone/npc.h b/zone/npc.h index 1521eb4de..c758ee19f 100644 --- a/zone/npc.h +++ b/zone/npc.h @@ -115,7 +115,7 @@ public: static NPC *SpawnNodeNPC(std::string name, std::string last_name, const glm::vec4 &position); static void SpawnGridNodeNPC(const glm::vec4 &position, int32 grid_id, int32 grid_number, int32 zoffset); - static void SpawnZonePointNodeNPC(std::string name, const glm::vec4 &position); + static NPC * SpawnZonePointNodeNPC(std::string name, const glm::vec4 &position); //abstract virtual function implementations requird by base abstract class virtual bool Death(Mob* killerMob, int32 damage, uint16 spell_id, EQ::skills::SkillType attack_skill);