diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index d1c7cf345f..7743a0e808 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -5093,6 +5093,19 @@ RENAME TABLE `starting_items_new` TO `starting_items`; .sql = R"( ALTER TABLE `items` MODIFY COLUMN `updated` datetime NULL DEFAULT NULL; )" + }, + ManifestEntry{ + .version = 9245, + .description = "2023_12_03_object_incline.sql", + .check = "SHOW COLUMNS FROM `object` LIKE 'incline'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `object` +CHANGE COLUMN `unknown08` `size_percentage` float NOT NULL DEFAULT 0 AFTER `icon`; +CHANGE COLUMN `unknown10` `solid_type` mediumint(5) NOT NULL DEFAULT 0 AFTER `size`, +CHANGE COLUMN `unknown20` `incline` int(11) NOT NULL DEFAULT 0 AFTER `solid_type`; +)" } // -- template; copy/paste this when you need to create a new entry diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index fc47f43e62..0d68c1810e 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -2594,11 +2594,11 @@ struct BookButton_Struct struct Object_Struct { /*00*/ uint32 linked_list_addr[2];// They are, get this, prev and next, ala linked list /*08*/ float size; // -/*10*/ uint16 solidtype; // +/*10*/ uint16 solid_type; // /*12*/ uint32 drop_id; // Unique object id for zone /*16*/ uint16 zone_id; // Redudant, but: Zone the object appears in /*18*/ uint16 zone_instance; // -/*20*/ uint32 unknown020; // +/*20*/ uint32 incline; // /*24*/ uint32 unknown024; // /*28*/ float tilt_x; /*32*/ float tilt_y; diff --git a/common/repositories/base/base_object_repository.h b/common/repositories/base/base_object_repository.h index 34e42debff..1a6c894cc4 100644 --- a/common/repositories/base/base_object_repository.h +++ b/common/repositories/base/base_object_repository.h @@ -16,6 +16,7 @@ #include "../../strings.h" #include + class BaseObjectRepository { public: struct Object { @@ -31,9 +32,9 @@ class BaseObjectRepository { std::string objectname; int32_t type; int32_t icon; - int32_t unknown08; - int32_t unknown10; - int32_t unknown20; + float size_percentage; + int32_t solid_type; + int32_t incline; int32_t unknown24; int32_t unknown60; int32_t unknown64; @@ -71,9 +72,9 @@ class BaseObjectRepository { "objectname", "type", "icon", - "unknown08", - "unknown10", - "unknown20", + "size_percentage", + "solid_type", + "incline", "unknown24", "unknown60", "unknown64", @@ -107,9 +108,9 @@ class BaseObjectRepository { "objectname", "type", "icon", - "unknown08", - "unknown10", - "unknown20", + "size_percentage", + "solid_type", + "incline", "unknown24", "unknown60", "unknown64", @@ -177,9 +178,9 @@ class BaseObjectRepository { e.objectname = ""; e.type = 0; e.icon = 0; - e.unknown08 = 0; - e.unknown10 = 0; - e.unknown20 = 0; + e.size_percentage = 0; + e.solid_type = 0; + e.incline = 0; e.unknown24 = 0; e.unknown60 = 0; e.unknown64 = 0; @@ -220,8 +221,9 @@ class BaseObjectRepository { { auto results = db.QueryDatabase( fmt::format( - "{} WHERE id = {} LIMIT 1", + "{} WHERE {} = {} LIMIT 1", BaseSelect(), + PrimaryKey(), object_id ) ); @@ -242,9 +244,9 @@ class BaseObjectRepository { e.objectname = row[9] ? row[9] : ""; e.type = static_cast(atoi(row[10])); e.icon = static_cast(atoi(row[11])); - e.unknown08 = static_cast(atoi(row[12])); - e.unknown10 = static_cast(atoi(row[13])); - e.unknown20 = static_cast(atoi(row[14])); + e.size_percentage = strtof(row[12], nullptr); + e.solid_type = static_cast(atoi(row[13])); + e.incline = static_cast(atoi(row[14])); e.unknown24 = static_cast(atoi(row[15])); e.unknown60 = static_cast(atoi(row[16])); e.unknown64 = static_cast(atoi(row[17])); @@ -304,9 +306,9 @@ class BaseObjectRepository { v.push_back(columns[9] + " = '" + Strings::Escape(e.objectname) + "'"); v.push_back(columns[10] + " = " + std::to_string(e.type)); v.push_back(columns[11] + " = " + std::to_string(e.icon)); - v.push_back(columns[12] + " = " + std::to_string(e.unknown08)); - v.push_back(columns[13] + " = " + std::to_string(e.unknown10)); - v.push_back(columns[14] + " = " + std::to_string(e.unknown20)); + v.push_back(columns[12] + " = " + std::to_string(e.size_percentage)); + v.push_back(columns[13] + " = " + std::to_string(e.solid_type)); + v.push_back(columns[14] + " = " + std::to_string(e.incline)); v.push_back(columns[15] + " = " + std::to_string(e.unknown24)); v.push_back(columns[16] + " = " + std::to_string(e.unknown60)); v.push_back(columns[17] + " = " + std::to_string(e.unknown64)); @@ -355,9 +357,9 @@ class BaseObjectRepository { v.push_back("'" + Strings::Escape(e.objectname) + "'"); v.push_back(std::to_string(e.type)); v.push_back(std::to_string(e.icon)); - v.push_back(std::to_string(e.unknown08)); - v.push_back(std::to_string(e.unknown10)); - v.push_back(std::to_string(e.unknown20)); + v.push_back(std::to_string(e.size_percentage)); + v.push_back(std::to_string(e.solid_type)); + v.push_back(std::to_string(e.incline)); v.push_back(std::to_string(e.unknown24)); v.push_back(std::to_string(e.unknown60)); v.push_back(std::to_string(e.unknown64)); @@ -414,9 +416,9 @@ class BaseObjectRepository { v.push_back("'" + Strings::Escape(e.objectname) + "'"); v.push_back(std::to_string(e.type)); v.push_back(std::to_string(e.icon)); - v.push_back(std::to_string(e.unknown08)); - v.push_back(std::to_string(e.unknown10)); - v.push_back(std::to_string(e.unknown20)); + v.push_back(std::to_string(e.size_percentage)); + v.push_back(std::to_string(e.solid_type)); + v.push_back(std::to_string(e.incline)); v.push_back(std::to_string(e.unknown24)); v.push_back(std::to_string(e.unknown60)); v.push_back(std::to_string(e.unknown64)); @@ -477,9 +479,9 @@ class BaseObjectRepository { e.objectname = row[9] ? row[9] : ""; e.type = static_cast(atoi(row[10])); e.icon = static_cast(atoi(row[11])); - e.unknown08 = static_cast(atoi(row[12])); - e.unknown10 = static_cast(atoi(row[13])); - e.unknown20 = static_cast(atoi(row[14])); + e.size_percentage = strtof(row[12], nullptr); + e.solid_type = static_cast(atoi(row[13])); + e.incline = static_cast(atoi(row[14])); e.unknown24 = static_cast(atoi(row[15])); e.unknown60 = static_cast(atoi(row[16])); e.unknown64 = static_cast(atoi(row[17])); @@ -531,9 +533,9 @@ class BaseObjectRepository { e.objectname = row[9] ? row[9] : ""; e.type = static_cast(atoi(row[10])); e.icon = static_cast(atoi(row[11])); - e.unknown08 = static_cast(atoi(row[12])); - e.unknown10 = static_cast(atoi(row[13])); - e.unknown20 = static_cast(atoi(row[14])); + e.size_percentage = strtof(row[12], nullptr); + e.solid_type = static_cast(atoi(row[13])); + e.incline = static_cast(atoi(row[14])); e.unknown24 = static_cast(atoi(row[15])); e.unknown60 = static_cast(atoi(row[16])); e.unknown64 = static_cast(atoi(row[17])); diff --git a/common/version.h b/common/version.h index c602e7cc3c..be5294acb5 100644 --- a/common/version.h +++ b/common/version.h @@ -42,7 +42,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9244 +#define CURRENT_BINARY_DATABASE_VERSION 9245 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9040 diff --git a/zone/client.cpp b/zone/client.cpp index 367e481c2c..7ee10f543f 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -10355,6 +10355,16 @@ void Client::SetDoorToolEntityId(uint16 door_tool_entity_id) Client::m_door_tool_entity_id = door_tool_entity_id; } +uint16 Client::GetObjectToolEntityId() const +{ + return m_object_tool_entity_id; +} + +void Client::SetObjectToolEntityId(uint16 object_tool_entity_id) +{ + Client::m_object_tool_entity_id = object_tool_entity_id; +} + int Client::GetIPExemption() { return database.GetIPExemption(GetIPString()); diff --git a/zone/client.h b/zone/client.h index 1c435bd557..9e8087e0ec 100644 --- a/zone/client.h +++ b/zone/client.h @@ -1814,9 +1814,12 @@ class Client : public Mob bool dev_tools_enabled; uint16 m_door_tool_entity_id; + uint16 m_object_tool_entity_id; public: uint16 GetDoorToolEntityId() const; void SetDoorToolEntityId(uint16 door_tool_entity_id); + uint16 GetObjectToolEntityId() const; + void SetObjectToolEntityId(uint16 object_tool_entity_id); private: int32 max_end; diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index b2911a9c68..46c1ef804b 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -60,6 +60,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/repositories/criteria/content_filter_criteria.h" #include "../common/shared_tasks.h" #include "gm_commands/door_manipulation.h" +#include "gm_commands/object_manipulation.h" #include "client.h" #include "../common/repositories/account_repository.h" @@ -4650,6 +4651,20 @@ void Client::Handle_OP_ClickObject(const EQApplicationPacket *app) std::vector args = { object }; parse->EventPlayer(EVENT_CLICK_OBJECT, this, std::to_string(click_object->drop_id), GetID(), &args); } + + if (IsDevToolsEnabled()) { + SetObjectToolEntityId(entity->GetID()); + ObjectManipulation::CommandHeader(this); + Message( + Chat::White, + fmt::format( + "Object ({}) [{}] [{}]", + entity->CastToObject()->GetDBID(), + Saylink::Silent("#object edit", "Edit"), + Saylink::Silent("#object delete", "Delete") + ).c_str() + ); + } } // Observed in RoF after OP_ClickObjectAction: diff --git a/zone/command.cpp b/zone/command.cpp index 6a3098b6e8..bec051a3fb 100644 --- a/zone/command.cpp +++ b/zone/command.cpp @@ -866,6 +866,7 @@ void command_bot(Client *c, const Seperator *sep) #include "gm_commands/nukebuffs.cpp" #include "gm_commands/nukeitem.cpp" #include "gm_commands/object.cpp" +#include "gm_commands/object_manipulation.cpp" #include "gm_commands/path.cpp" #include "gm_commands/peqzone.cpp" #include "gm_commands/petitems.cpp" diff --git a/zone/gm_commands/door.cpp b/zone/gm_commands/door.cpp index 5a92e5f624..33764af5bb 100755 --- a/zone/gm_commands/door.cpp +++ b/zone/gm_commands/door.cpp @@ -1,6 +1,5 @@ #include "../client.h" #include "door_manipulation.h" -#include "../doors.h" void command_door(Client *c, const Seperator *sep) { diff --git a/zone/gm_commands/object.cpp b/zone/gm_commands/object.cpp index df8f090f8b..19a5d8db5f 100755 --- a/zone/gm_commands/object.cpp +++ b/zone/gm_commands/object.cpp @@ -1,1263 +1,7 @@ #include "../client.h" -#include "../object.h" -#include "../doors.h" +#include "object_manipulation.h" void command_object(Client *c, const Seperator *sep) { - if (!c) { - return; - } // Crash Suppressant: No client. How did we get here? - - // Save it here. We sometimes have need to refer to it in multiple places. - const char *usage_string = "Usage: #object List|Add|Edit|Move|Rotate|Save|Copy|Delete|Undo"; - - if ((!sep) || (sep->argnum == 0)) { - c->Message(Chat::White, usage_string); - return; - } - - Object *o = nullptr; - Object_Struct od; - Door_Struct *ds; - uint32 id = 0; - uint32 itemid = 0; - uint32 icon = 0; - uint32 instance = 0; - uint32 newid = 0; - uint16 radius; - EQApplicationPacket *app; - - bool bNewObject = false; - - float x2; - float y2; - - // Temporary object type for static objects to allow manipulation - // NOTE: Zone::LoadZoneObjects() currently loads this as an uint8, so max value is 255! - static const uint32 staticType = 255; - - // Case insensitive commands (List == list == LIST) - strlwr(sep->arg[1]); - - if (strcasecmp(sep->arg[1], "list") == 0) { - // Insufficient or invalid args - if ((sep->argnum < 2) || (sep->arg[2][0] < '0') || - ((sep->arg[2][0] > '9') && ((sep->arg[2][0] & 0xDF) != 'A'))) { - c->Message(Chat::White, "Usage: #object List All|(radius)"); - return; - } - - if ((sep->arg[2][0] & 0xDF) == 'A') { - radius = 0; // List All - } - else if ((radius = Strings::ToInt(sep->arg[2])) <= 0) { - radius = 500; - } // Invalid radius. Default to 500 units. - - if (radius == 0) - c->Message(Chat::White, "Objects within this zone:"); - else - c->Message(Chat::White, "Objects within %u units of your current location:", radius); - - std::string query; - if (radius) - query = StringFormat( - "SELECT id, xpos, ypos, zpos, heading, itemid, " - "objectname, type, icon, unknown08, unknown10, unknown20 " - "FROM object WHERE zoneid = %u AND version = %u " - "AND (xpos BETWEEN %.1f AND %.1f) " - "AND (ypos BETWEEN %.1f AND %.1f) " - "AND (zpos BETWEEN %.1f AND %.1f) " - "ORDER BY id", - zone->GetZoneID(), zone->GetInstanceVersion(), - c->GetX() - radius, // Yes, we're actually using a bounding box instead of a radius. - c->GetX() + radius, // Much less processing power used this way. - c->GetY() - radius, c->GetY() + radius, c->GetZ() - radius, c->GetZ() + radius - ); - else - query = StringFormat( - "SELECT id, xpos, ypos, zpos, heading, itemid, " - "objectname, type, icon, unknown08, unknown10, unknown20 " - "FROM object WHERE zoneid = %u AND version = %u " - "ORDER BY id", - zone->GetZoneID(), zone->GetInstanceVersion()); - - auto results = content_db.QueryDatabase(query); - if (!results.Success()) { - c->Message(Chat::White, "Error in objects query"); - return; - } - - for (auto row = results.begin(); row != results.end(); ++row) { - id = Strings::ToInt(row[0]); - od.x = Strings::ToFloat(row[1]); - od.y = Strings::ToFloat(row[2]); - od.z = Strings::ToFloat(row[3]); - od.heading = Strings::ToFloat(row[4]); - itemid = Strings::ToInt(row[5]); - strn0cpy(od.object_name, row[6], sizeof(od.object_name)); - od.object_name[sizeof(od.object_name) - 1] = - '\0'; // Required if strlen(row[col++]) exactly == sizeof(object_name) - - od.object_type = Strings::ToInt(row[7]); - icon = Strings::ToInt(row[8]); - od.size = Strings::ToInt(row[9]); - od.solidtype = Strings::ToInt(row[10]); - od.unknown020 = Strings::ToInt(row[11]); - - switch (od.object_type) { - case 0: // Static Object - case staticType: // Static Object unlocked for changes - if (od.size == 0) // Unknown08 field is optional Size parameter for static objects - od.size = 100; // Static object default Size is 100% - - c->Message( - Chat::White, "- STATIC Object (%s): id %u, x %.1f, y %.1f, z %.1f, h %.1f, model %s, " - "size %u, solidtype %u, incline %u", - (od.object_type == 0) ? "locked" : "unlocked", id, od.x, od.y, od.z, - od.heading, od.object_name, od.size, od.solidtype, od.unknown020 - ); - break; - - case OT_DROPPEDITEM: // Ground Spawn - c->Message( - Chat::White, "- TEMPORARY Object: id %u, x %.1f, y %.1f, z %.1f, h %.1f, itemid %u, " - "model %s, icon %u", - id, od.x, od.y, od.z, od.heading, itemid, od.object_name, icon - ); - break; - - default: // All others == Tradeskill Objects - c->Message( - Chat::White, "- TRADESKILL Object: id %u, x %.1f, y %.1f, z %.1f, h %.1f, model %s, " - "type %u, icon %u", - id, od.x, od.y, od.z, od.heading, od.object_name, od.object_type, icon - ); - break; - } - } - - c->Message(Chat::White, "%u object%s found", results.RowCount(), (results.RowCount() == 1) ? "" : "s"); - return; - } - - if (strcasecmp(sep->arg[1], "add") == 0) { - // Insufficient or invalid arguments - if ((sep->argnum < 3) || - ((sep->arg[3][0] == '\0') && (sep->arg[4][0] < '0') && (sep->arg[4][0] > '9'))) { - c->Message( - Chat::White, "Usage: (Static Object): #object Add [ObjectID] 0 Model [SizePercent] " - "[SolidType] [Incline]" - ); - c->Message(Chat::White, "Usage: (Tradeskill Object): #object Add [ObjectID] TypeNum Model Icon"); - c->Message( - Chat::White, "- Notes: Model must start with a letter, max length 16. SolidTypes = 0 (Solid), " - "1 (Sometimes Non-Solid)" - ); - return; - } - - int col; - - if (sep->argnum > 3) { // Model name in arg3? - if ((sep->arg[3][0] <= '9') && (sep->arg[3][0] >= '0')) { - // Nope, user must have specified ObjectID. Extract it. - id = Strings::ToInt(sep->arg[2]); - col = 1; // Bump all other arguments one to the right. Model is in arg4. - } - else { - // Yep, arg3 is non-numeric, ObjectID must be omitted and model must be arg3 - id = 0; - col = 0; - } - } - else { - // Nope, only 3 args. Object ID must be omitted and arg3 must be model. - id = 0; - col = 0; - } - - memset(&od, 0, sizeof(od)); - - od.object_type = Strings::ToInt(sep->arg[2 + col]); - - switch (od.object_type) { - case 0: // Static Object - if ((sep->argnum - col) > 3) { - od.size = Strings::ToInt(sep->arg[4 + col]); // Size specified - - if ((sep->argnum - col) > 4) { - od.solidtype = Strings::ToInt(sep->arg[5 + col]); // SolidType specified - - if ((sep->argnum - col) > 5) { - od.unknown020 = Strings::ToInt(sep->arg[6 + col]); - } // Incline specified - } - } - break; - - case 1: // Ground Spawn - c->Message( - Chat::White, "ERROR: Object Type 1 is used for temporarily spawned ground spawns and dropped " - "items, which are not supported with #object. See the 'ground_spawns' table in " - "the database." - ); - return; - - default: // Everything else == Tradeskill Object - icon = ((sep->argnum - col) > 3) ? Strings::ToInt(sep->arg[4 + col]) : 0; - - if (icon == 0) { - c->Message(Chat::White, "ERROR: Required property 'Icon' not specified for Tradeskill Object"); - return; - } - - break; - } - - od.x = c->GetX(); - od.y = c->GetY(); - od.z = c->GetZ() - (c->GetSize() * 0.625f); - od.heading = c->GetHeading(); - - std::string query; - if (id) { - // ID specified. Verify that it doesn't already exist. - query = StringFormat("SELECT COUNT(*) FROM object WHERE ID = %u", id); - auto results = content_db.QueryDatabase(query); - if (results.Success() && results.RowCount() != 0) { - auto row = results.begin(); - if (Strings::ToInt(row[0]) > 0) { // Yep, in database already. - id = 0; - } - } - - // Not in database. Already spawned, just not saved? - // Yep, already spawned. - if (id && entity_list.FindObject(id)) { - id = 0; - } - - if (id == 0) { - c->Message(Chat::White, "ERROR: An object already exists with the id %u", Strings::ToInt(sep->arg[2])); - return; - } - } - - int objectsFound = 0; - // Verify no other objects already in this spot (accidental double-click of Hotkey?) - query = StringFormat( - "SELECT COUNT(*) FROM object WHERE zoneid = %u " - "AND version=%u AND (xpos BETWEEN %.1f AND %.1f) " - "AND (ypos BETWEEN %.1f AND %.1f) " - "AND (zpos BETWEEN %.1f AND %.1f)", - zone->GetZoneID(), zone->GetInstanceVersion(), od.x - 0.2f, - od.x + 0.2f, // Yes, we're actually using a bounding box instead of a radius. - od.y - 0.2f, od.y + 0.2f, // Much less processing power used this way. - od.z - 0.2f, od.z + 0.2f - ); // It's pretty forgiving, though, allowing for close-proximity objects - - auto results = content_db.QueryDatabase(query); - if (results.Success() && results.RowCount() != 0) { - auto row = results.begin(); - objectsFound = Strings::ToInt(row[0]); // Number of nearby objects from database - } - - // No objects found in database too close. How about spawned but not yet saved? - if (objectsFound == 0 && entity_list.FindNearbyObject(od.x, od.y, od.z, 0.2f)) { - objectsFound = 1; - } - - if (objectsFound) { - c->Message(Chat::White, "ERROR: Object already at this location."); - return; - } - - // Strip any single quotes from objectname (SQL injection FTL!) - strn0cpy(od.object_name, sep->arg[3 + col], sizeof(od.object_name)); - - uint32 len = strlen(od.object_name); - for (col = 0; col < (uint32) len; col++) { - if (od.object_name[col] != '\'') { - continue; - } - - // Uh oh, 1337 h4x0r monkeying around! Strip that apostrophe! - memcpy(&od.object_name[col], &od.object_name[col + 1], len - col); - len--; - col--; - } - - strupr(od.object_name); // Model names are always upper-case. - - if ((od.object_name[0] < 'A') || (od.object_name[0] > 'Z')) { - c->Message(Chat::White, "ERROR: Model name must start with a letter."); - return; - } - - if (id == 0) { - // No ID specified. Get a best-guess next number from the database - // If there's a problem retrieving an ID from the database, it'll end up being object # 1. No - // biggie. - - query = "SELECT MAX(id) FROM object"; - results = content_db.QueryDatabase(query); - if (results.Success() && results.RowCount() != 0) { - auto row = results.begin(); - id = Strings::ToInt(row[0]); - } - - id++; - } - - // Make sure not to overwrite already-spawned objects that haven't been saved yet. - while (o = entity_list.FindObject(id)) - id++; - - // Static object - if (od.object_type == 0) { - od.object_type = staticType; - } // Temporary. We'll make it 0 when we Save - - od.zone_id = zone->GetZoneID(); - od.zone_instance = zone->GetInstanceVersion(); - - o = new Object(id, od.object_type, icon, od, nullptr); - - // Add to our zone entity list and spawn immediately for all clients - entity_list.AddObject(o, true); - - // Bump player back to avoid getting stuck inside new object - - x2 = 10.0f * sin(c->GetHeading() / 256.0f * 3.14159265f); - y2 = 10.0f * cos(c->GetHeading() / 256.0f * 3.14159265f); - c->MovePC(c->GetX() - x2, c->GetY() - y2, c->GetZ(), c->GetHeading()); - - c->Message( - Chat::White, "Spawning object with tentative id %u at location (%.1f, %.1f, %.1f heading %.1f). Use " - "'#object Save' to save to database when satisfied with placement.", - id, od.x, od.y, od.z, od.heading - ); - - // Temporary Static Object - if (od.object_type == staticType) - c->Message( - Chat::White, "- Note: Static Object will act like a tradeskill container and will not reflect " - "size, solidtype, or incline values until you commit with '#object Save', after " - "which it will be unchangeable until you use '#object Edit' and zone back in." - ); - - return; - } - - if (strcasecmp(sep->arg[1], "edit") == 0) { - - if ((sep->argnum < 2) || ((id = Strings::ToInt(sep->arg[2])) < 1)) { - c->Message(Chat::White, "Usage: #object Edit (ObjectID) [PropertyName] [NewValue]"); - c->Message(Chat::White, "- Static Object (Type 0) Properties: model, type, size, solidtype, incline"); - c->Message(Chat::White, "- Tradeskill Object (Type 2+) Properties: model, type, icon"); - - return; - } - - o = entity_list.FindObject(id); - - // Object already available in-zone? - if (o) { - // Yep, looks like we can make real-time changes. - if (sep->argnum < 4) { - // Or not. '#object Edit (ObjectID)' called without PropertyName and NewValue - c->Message(Chat::White, "Note: Object %u already unlocked and ready for changes", id); - return; - } - } - else { - // Object not found in-zone in a modifiable form. Check for valid matching circumstances. - std::string query = StringFormat("SELECT zoneid, version, type FROM object WHERE id = %u", id); - auto results = content_db.QueryDatabase(query); - if (!results.Success() || results.RowCount() == 0) { - c->Message(Chat::White, "ERROR: Object %u not found", id); - return; - } - - auto row = results.begin(); - od.zone_id = Strings::ToInt(row[0]); - od.zone_instance = Strings::ToInt(row[1]); - od.object_type = Strings::ToInt(row[2]); - uint32 objectsFound = 1; - - // Object not in this zone? - if (od.zone_id != zone->GetZoneID()) { - c->Message(Chat::White, "ERROR: Object %u not in this zone.", id); - return; - } - - // Object not in this instance? - if (od.zone_instance != zone->GetInstanceVersion()) { - c->Message(Chat::White, "ERROR: Object %u not part of this instance version.", id); - return; - } - - switch (od.object_type) { - case 0: // Static object needing unlocking - // Convert to tradeskill object temporarily for changes - query = StringFormat("UPDATE object SET type = %u WHERE id = %u", staticType, id); - - content_db.QueryDatabase(query); - - c->Message( - Chat::White, "Static Object %u unlocked for editing. You must zone out and back in to " - "make your changes, then commit them with '#object Save'.", - id - ); - if (sep->argnum >= 4) { - c->Message( - Chat::White, "NOTE: The change you specified has not been applied, since the " - "static object had not been unlocked for editing yet." - ); - } - return; - - case OT_DROPPEDITEM: - c->Message( - Chat::White, "ERROR: Object %u is a temporarily spawned ground spawn or dropped item, " - "which cannot be manipulated with #object. See the 'ground_spawns' table " - "in the database.", - id - ); - return; - - case staticType: - c->Message( - Chat::White, "ERROR: Object %u has been unlocked for editing, but you must zone out " - "and back in for your client to refresh its object table before you can " - "make changes to it.", - id - ); - return; - - default: - // Unknown error preventing us from seeing the object in the zone. - c->Message(Chat::White, "ERROR: Unknown problem attempting to manipulate object %u", id); - return; - } - } - - // If we're here, we have a manipulable object ready for changes. - strlwr(sep->arg[3]); // Case insensitive PropertyName - strupr(sep->arg[4]); // In case it's model name, which should always be upper-case - - // Read current object info for reference - icon = o->GetIcon(); - o->GetObjectData(&od); - - // We'll be a little more picky with property names, to prevent errors. Check against the whole word. - if (strcmp(sep->arg[3], "model") == 0) { - - if ((sep->arg[4][0] < 'A') || (sep->arg[4][0] > 'Z')) { - c->Message(Chat::White, "ERROR: Model names must begin with a letter."); - return; - } - - strn0cpy(od.object_name, sep->arg[4], sizeof(od.object_name)); - - o->SetObjectData(&od); - - c->Message(Chat::White, "Object %u now being rendered with model '%s'", id, od.object_name); - } - else if (strcmp(sep->arg[3], "type") == 0) { - if ((sep->arg[4][0] < '0') || (sep->arg[4][0] > '9')) { - c->Message(Chat::White, "ERROR: Invalid type number"); - return; - } - - od.object_type = Strings::ToInt(sep->arg[4]); - - switch (od.object_type) { - case 0: - // Convert Static Object to temporary changeable type - od.object_type = staticType; - c->Message( - Chat::White, "Note: Static Object will still act like tradeskill object and will not " - "reflect size, solidtype, or incline settings until committed to the " - "database with '#object Save', after which it will be unchangeable until " - "it is unlocked again with '#object Edit'." - ); - break; - - case OT_DROPPEDITEM: - c->Message( - Chat::White, "ERROR: Object Type 1 is used for temporarily spawned ground spawns and " - "dropped items, which are not supported with #object. See the " - "'ground_spawns' table in the database." - ); - return; - - default: - c->Message(Chat::White, "Object %u changed to Tradeskill Object Type %u", id, od.object_type); - break; - } - - o->SetType(od.object_type); - } - else if (strcmp(sep->arg[3], "size") == 0) { - if (od.object_type != staticType) { - c->Message( - 0, "ERROR: Object %u is not a Static Object and does not support the Size property", - id - ); - return; - } - - if ((sep->arg[4][0] < '0') || (sep->arg[4][0] > '9')) { - c->Message(Chat::White, "ERROR: Invalid size specified. Please enter a number."); - return; - } - - od.size = Strings::ToInt(sep->arg[4]); - o->SetObjectData(&od); - - if (od.size == 0) { // 0 == unspecified == 100% - od.size = 100; - } - - c->Message( - Chat::White, "Static Object %u set to %u%% size. Size will take effect when you commit to the " - "database with '#object Save', after which the object will be unchangeable until " - "you unlock it again with '#object Edit' and zone out and back in.", - id, od.size - ); - } - else if (strcmp(sep->arg[3], "solidtype") == 0) { - - if (od.object_type != staticType) { - c->Message( - Chat::White, "ERROR: Object %u is not a Static Object and does not support the " - "SolidType property", - id - ); - return; - } - - if ((sep->arg[4][0] < '0') || (sep->arg[4][0] > '9')) { - c->Message(Chat::White, "ERROR: Invalid solidtype specified. Please enter a number."); - return; - } - - od.solidtype = Strings::ToInt(sep->arg[4]); - o->SetObjectData(&od); - - c->Message( - Chat::White, "Static Object %u set to SolidType %u. Change will take effect when you commit " - "to the database with '#object Save'. Support for this property is on a " - "per-model basis, mostly seen in smaller objects such as chests and tables.", - id, od.solidtype - ); - } - else if (strcmp(sep->arg[3], "icon") == 0) { - - if ((od.object_type < 2) || (od.object_type == staticType)) { - c->Message( - Chat::White, "ERROR: Object %u is not a Tradeskill Object and does not support the " - "Icon property", - id - ); - return; - } - - if ((icon = Strings::ToInt(sep->arg[4])) == 0) { - c->Message(Chat::White, "ERROR: Invalid Icon specified. Please enter an icon number."); - return; - } - - o->SetIcon(icon); - c->Message(Chat::White, "Tradeskill Object %u icon set to %u", id, icon); - } - else if (strcmp(sep->arg[3], "incline") == 0) { - if (od.object_type != staticType) { - c->Message( - 0, - "ERROR: Object %u is not a Static Object and does not support the Incline property", - id - ); - return; - } - - if ((sep->arg[4][0] < '0') || (sep->arg[4][0] > '9')) { - c->Message( - 0, - "ERROR: Invalid Incline specified. Please enter a number. Normal range is 0-512." - ); - return; - } - - od.unknown020 = Strings::ToInt(sep->arg[4]); - o->SetObjectData(&od); - - c->Message( - Chat::White, "Static Object %u set to %u incline. Incline will take effect when you commit to " - "the database with '#object Save', after which the object will be unchangeable " - "until you unlock it again with '#object Edit' and zone out and back in.", - id, od.unknown020 - ); - } - else { - c->Message(Chat::White, "ERROR: Unrecognized property name: %s", sep->arg[3]); - return; - } - - // Repop object to have it reflect the change. - app = new EQApplicationPacket(); - o->CreateDeSpawnPacket(app); - entity_list.QueueClients(0, app); - safe_delete(app); - - app = new EQApplicationPacket(); - o->CreateSpawnPacket(app); - entity_list.QueueClients(0, app); - safe_delete(app); - return; - } - - if (strcasecmp(sep->arg[1], "move") == 0) { - - if ((sep->argnum < 2) || // Not enough arguments - ((id = Strings::ToInt(sep->arg[2])) == 0) || // ID not specified - (((sep->arg[3][0] < '0') || (sep->arg[3][0] > '9')) && ((sep->arg[3][0] & 0xDF) != 'T') && - (sep->arg[3][0] != '-') && (sep->arg[3][0] != '.'))) { // Location argument not specified correctly - c->Message(Chat::White, "Usage: #object Move (ObjectID) ToMe|(x y z [h])"); - return; - } - - if (!(o = entity_list.FindObject(id))) { - std::string query = StringFormat("SELECT zoneid, version, type FROM object WHERE id = %u", id); - auto results = content_db.QueryDatabase(query); - if (!results.Success() || results.RowCount() == 0) { - c->Message(Chat::White, "ERROR: Object %u not found", id); - return; - } - - auto row = results.begin(); - od.zone_id = Strings::ToInt(row[0]); - od.zone_instance = Strings::ToInt(row[1]); - od.object_type = Strings::ToInt(row[2]); - - if (od.zone_id != zone->GetZoneID()) { - c->Message(Chat::White, "ERROR: Object %u is not in this zone", id); - return; - } - - if (od.zone_instance != zone->GetInstanceVersion()) { - c->Message(Chat::White, "ERROR: Object %u is not in this instance version", id); - return; - } - - switch (od.object_type) { - case 0: - c->Message( - Chat::White, "ERROR: Object %u is not yet unlocked for editing. Use '#object Edit' " - "then zone out and back in to move it.", - id - ); - return; - - case staticType: - c->Message( - Chat::White, "ERROR: Object %u has been unlocked for editing, but you must zone out " - "and back in before your client sees the change and will allow you to " - "move it.", - id - ); - return; - - case 1: - c->Message( - Chat::White, "ERROR: Object %u is a temporary spawned object and cannot be " - "manipulated with #object. See the 'ground_spawns' table in the " - "database.", - id - ); - return; - - default: - c->Message(Chat::White, "ERROR: Object %u not located in zone.", id); - return; - } - } - - // Move To Me - if ((sep->arg[3][0] & 0xDF) == 'T') { - od.x = c->GetX(); - od.y = c->GetY(); - od.z = c->GetZ() - - (c->GetSize() * - 0.625f); // Compensate for #loc bumping up Z coordinate by 62.5% of character's size. - - o->SetHeading(c->GetHeading()); - - // Bump player back to avoid getting stuck inside object - - x2 = 10.0f * std::sin(c->GetHeading() / 256.0f * 3.14159265f); - y2 = 10.0f * std::cos(c->GetHeading() / 256.0f * 3.14159265f); - c->MovePC(c->GetX() - x2, c->GetY() - y2, c->GetZ(), c->GetHeading()); - } // Move to x, y, z [h] - else { - od.x = Strings::ToFloat(sep->arg[3]); - if (sep->argnum > 3) { - od.y = Strings::ToFloat(sep->arg[4]); - } - else { - o->GetLocation(nullptr, &od.y, nullptr); - } - - if (sep->argnum > 4) { - od.z = Strings::ToFloat(sep->arg[5]); - } - else { - o->GetLocation(nullptr, nullptr, &od.z); - } - - if (sep->argnum > 5) { - o->SetHeading(Strings::ToFloat(sep->arg[6])); - } - } - - o->SetLocation(od.x, od.y, od.z); - - // Despawn and respawn object to reflect change - app = new EQApplicationPacket(); - o->CreateDeSpawnPacket(app); - entity_list.QueueClients(0, app); - safe_delete(app); - - app = new EQApplicationPacket(); - o->CreateSpawnPacket(app); - entity_list.QueueClients(0, app); - safe_delete(app); - return; - } - - if (strcasecmp(sep->arg[1], "rotate") == 0) { - // Insufficient or invalid arguments - if ((sep->argnum < 3) || ((id = Strings::ToInt(sep->arg[2])) == 0)) { - c->Message(Chat::White, "Usage: #object Rotate (ObjectID) (Heading, 0-512)"); - return; - } - - if ((o = entity_list.FindObject(id)) == nullptr) { - c->Message( - Chat::White, "ERROR: Object %u not found in zone, or is a static object not yet unlocked with " - "'#object Edit' for editing.", - id - ); - return; - } - - o->SetHeading(Strings::ToFloat(sep->arg[3])); - - // Despawn and respawn object to reflect change - app = new EQApplicationPacket(); - o->CreateDeSpawnPacket(app); - entity_list.QueueClients(0, app); - safe_delete(app); - - app = new EQApplicationPacket(); - o->CreateSpawnPacket(app); - entity_list.QueueClients(0, app); - safe_delete(app); - return; - } - - if (strcasecmp(sep->arg[1], "save") == 0) { - // Insufficient or invalid arguments - if ((sep->argnum < 2) || ((id = Strings::ToInt(sep->arg[2])) == 0)) { - c->Message(Chat::White, "Usage: #object Save (ObjectID)"); - return; - } - - o = entity_list.FindObject(id); - - od.zone_id = 0; - od.zone_instance = 0; - od.object_type = 0; - - // If this ID isn't in the database yet, it's a new object - bNewObject = true; - std::string query = StringFormat("SELECT zoneid, version, type FROM object WHERE id = %u", id); - auto results = content_db.QueryDatabase(query); - if (results.Success() && results.RowCount() != 0) { - auto row = results.begin(); - od.zone_id = Strings::ToInt(row[0]); - od.zone_instance = Strings::ToInt(row[1]); - od.object_type = Strings::ToInt(row[2]); - - // ID already in database. Not a new object. - bNewObject = false; - } - - if (!o) { - // Object not found in zone. Can't save an object we can't see. - - if (bNewObject) { - c->Message(Chat::White, "ERROR: Object %u not found", id); - return; - } - - if (od.zone_id != zone->GetZoneID()) { - c->Message(Chat::White, "ERROR: Wrong Object ID. %u is not part of this zone.", id); - return; - } - - if (od.zone_instance != zone->GetInstanceVersion()) { - c->Message(Chat::White, "ERROR: Wrong Object ID. %u is not part of this instance version.", id); - return; - } - - if (od.object_type == 0) { - c->Message( - Chat::White, "ERROR: Static Object %u has already been committed. Use '#object Edit " - "%u' and zone out and back in to make changes.", - id, id - ); - return; - } - - if (od.object_type == 1) { - c->Message( - Chat::White, "ERROR: Object %u is a temporarily spawned ground spawn or dropped item, " - "which is not supported with #object. See the 'ground_spawns' table in " - "the database.", - id - ); - return; - } - - c->Message(Chat::White, "ERROR: Object %u not found.", id); - return; - } - - // Oops! Another GM already saved an object with our id from another zone. - // We'll have to get a new one. - if ((od.zone_id > 0) && (od.zone_id != zone->GetZoneID())) { - id = 0; - } - - // Oops! Another GM already saved an object with our id from another instance. - // We'll have to get a new one. - if ((id > 0) && (od.zone_instance != zone->GetInstanceVersion())) { - id = 0; - } - - // If we're asking for a new ID, it's a new object. - bNewObject |= (id == 0); - - o->GetObjectData(&od); - od.object_type = o->GetType(); - icon = o->GetIcon(); - - // We're committing to the database now. Return temporary object type to actual. - if (od.object_type == staticType) { - od.object_type = 0; - } - - if (!bNewObject) { - query = StringFormat( - "UPDATE object SET zoneid = %u, version = %u, " - "xpos = %.1f, ypos=%.1f, zpos=%.1f, heading=%.1f, " - "objectname = '%s', type = %u, icon = %u, " - "unknown08 = %u, unknown10 = %u, unknown20 = %u " - "WHERE ID = %u", - zone->GetZoneID(), zone->GetInstanceVersion(), od.x, od.y, od.z, - od.heading, od.object_name, od.object_type, icon, od.size, - od.solidtype, od.unknown020, id - ); - } - else if (id == 0) { - query = StringFormat( - "INSERT INTO object " - "(zoneid, version, xpos, ypos, zpos, heading, objectname, " - "type, icon, unknown08, unknown10, unknown20) " - "VALUES (%u, %u, %.1f, %.1f, %.1f, %.1f, '%s', %u, %u, %u, %u, %u)", - zone->GetZoneID(), zone->GetInstanceVersion(), od.x, od.y, od.z, - od.heading, od.object_name, od.object_type, icon, od.size, - od.solidtype, od.unknown020 - ); - } - else { - query = StringFormat( - "INSERT INTO object " - "(id, zoneid, version, xpos, ypos, zpos, heading, objectname, " - "type, icon, unknown08, unknown10, unknown20) " - "VALUES (%u, %u, %u, %.1f, %.1f, %.1f, %.1f, '%s', %u, %u, %u, %u, %u)", - id, zone->GetZoneID(), zone->GetInstanceVersion(), od.x, od.y, od.z, - od.heading, od.object_name, od.object_type, icon, od.size, - od.solidtype, od.unknown020 - ); - } - - results = content_db.QueryDatabase(query); - if (!results.Success()) { - c->Message(Chat::White, "Database Error: %s", results.ErrorMessage().c_str()); - return; - } - - if (results.RowsAffected() == 0) { - // No change made, but no error message given - c->Message(Chat::White, "Database Error: Could not save change to Object %u", id); - return; - } - - if (bNewObject) { - if (newid == results.LastInsertedID()) { - c->Message(Chat::White, "Saved new Object %u to database", id); - return; - } - - c->Message(Chat::White, "Saved Object. NOTE: Database returned a new ID number for object: %u", newid); - id = newid; - return; - } - - c->Message(Chat::White, "Saved changes to Object %u", id); - newid = id; - - if (od.object_type == 0) { - // Static Object - Respawn as nonfunctional door - - app = new EQApplicationPacket(); - o->CreateDeSpawnPacket(app); - entity_list.QueueClients(0, app); - safe_delete(app); - - entity_list.RemoveObject(o->GetID()); - - auto door = DoorsRepository::NewEntity(); - - door.zone = zone->GetShortName(); - - door.id = 1000000000 + id; // Out of range of normal use for doors.id - door.doorid = -1; // Client doesn't care if these are all the same door_id - door.pos_x = od.x; - door.pos_y = od.y; - door.pos_z = od.z; - door.heading = od.heading; - - door.name = od.object_name; - - // Strip trailing "_ACTORDEF" if present. Client won't accept it for doors. - int pos = door.name.size() - strlen("_ACTORDEF"); - if (pos > 0 && door.name.compare(pos, std::string::npos, "_ACTORDEF") == 0) { - door.name.erase(pos); - } - - door.dest_zone = "NONE"; - - if ((door.size = od.size) == 0) { // unknown08 = optional size percentage - door.size = 100; - } - - door.opentype = od.solidtype; - - switch (door.opentype) // unknown10 = optional request_nonsolid (0 or 1 or experimental number) - { - case 0: - door.opentype = 31; - break; - - case 1: - door.opentype = 9; - break; - } - - door.incline = od.unknown020; // unknown20 = optional incline value - door.client_version_mask = 0xFFFFFFFF; - - Doors *doors = new Doors(door); - - entity_list.AddDoor(doors); - - app = new EQApplicationPacket(OP_SpawnDoor, sizeof(Door_Struct)); - ds = (Door_Struct *) app->pBuffer; - - memset(ds, 0, sizeof(Door_Struct)); - memcpy(ds->name, door.name.c_str(), 32); - ds->xPos = door.pos_x; - ds->yPos = door.pos_y; - ds->zPos = door.pos_z; - ds->heading = door.heading; - ds->incline = door.incline; - ds->size = door.size; - ds->doorId = door.doorid; - ds->opentype = door.opentype; - ds->unknown0052[9] = 1; // *ptr-1 and *ptr-3 from EntityList::MakeDoorSpawnPacket() - ds->unknown0052[11] = 1; - - entity_list.QueueClients(0, app); - safe_delete(app); - - c->Message( - Chat::White, "NOTE: Object %u is now a static object, and is unchangeable. To make future " - "changes, use '#object Edit' to convert it to a changeable form, then zone out " - "and back in.", - id - ); - } - return; - } - - if (strcasecmp(sep->arg[1], "copy") == 0) { - // Insufficient or invalid arguments - if ((sep->argnum < 3) || - (((sep->arg[2][0] & 0xDF) != 'A') && ((sep->arg[2][0] < '0') || (sep->arg[2][0] > '9')))) { - c->Message(Chat::White, "Usage: #object Copy All|(ObjectID) (InstanceVersion)"); - c->Message(Chat::White, "- Note: Only objects saved in the database can be copied to another instance."); - return; - } - - od.zone_instance = Strings::ToInt(sep->arg[3]); - - if (od.zone_instance == zone->GetInstanceVersion()) { - c->Message(Chat::White, "ERROR: Source and destination instance versions are the same."); - return; - } - - if ((sep->arg[2][0] & 0xDF) == 'A') { - // Copy All - - std::string query = - StringFormat( - "INSERT INTO object " - "(zoneid, version, xpos, ypos, zpos, heading, itemid, " - "objectname, type, icon, unknown08, unknown10, unknown20) " - "SELECT zoneid, %u, xpos, ypos, zpos, heading, itemid, " - "objectname, type, icon, unknown08, unknown10, unknown20 " - "FROM object WHERE zoneid = %u) AND version = %u", - od.zone_instance, zone->GetZoneID(), zone->GetInstanceVersion()); - auto results = content_db.QueryDatabase(query); - if (!results.Success()) { - c->Message(Chat::White, "Database Error: %s", results.ErrorMessage().c_str()); - return; - } - - c->Message( - Chat::White, "Copied %u object%s into instance version %u", results.RowCount(), - (results.RowCount() == 1) ? "" : "s", od.zone_instance - ); - return; - } - - id = Strings::ToInt(sep->arg[2]); - - std::string query = StringFormat( - "INSERT INTO object " - "(zoneid, version, xpos, ypos, zpos, heading, itemid, " - "objectname, type, icon, unknown08, unknown10, unknown20) " - "SELECT zoneid, %u, xpos, ypos, zpos, heading, itemid, " - "objectname, type, icon, unknown08, unknown10, unknown20 " - "FROM object WHERE id = %u AND zoneid = %u AND version = %u", - od.zone_instance, id, zone->GetZoneID(), zone->GetInstanceVersion()); - auto results = content_db.QueryDatabase(query); - if (results.Success() && results.RowsAffected() > 0) { - c->Message(Chat::White, "Copied Object %u into instance version %u", id, od.zone_instance); - return; - } - - // Couldn't copy the object. - - // got an error message - if (!results.Success()) { - c->Message(Chat::White, "Database Error: %s", results.ErrorMessage().c_str()); - return; - } - - // No database error returned. See if we can figure out why. - - query = StringFormat("SELECT zoneid, version FROM object WHERE id = %u", id); - results = content_db.QueryDatabase(query); - if (!results.Success()) { - return; - } - - if (results.RowCount() == 0) { - c->Message(Chat::White, "ERROR: Object %u not found", id); - return; - } - - auto row = results.begin(); - // Wrong ZoneID? - if (Strings::ToInt(row[0]) != zone->GetZoneID()) { - c->Message(Chat::White, "ERROR: Object %u is not part of this zone.", id); - return; - } - - // Wrong Instance Version? - if (Strings::ToInt(row[1]) != zone->GetInstanceVersion()) { - c->Message(Chat::White, "ERROR: Object %u is not part of this instance version.", id); - return; - } - - // Well, NO clue at this point. Just let 'em know something screwed up. - c->Message( - Chat::White, "ERROR: Unknown database error copying Object %u to instance version %u", id, - od.zone_instance - ); - return; - } - - if (strcasecmp(sep->arg[1], "delete") == 0) { - - if ((sep->argnum < 2) || ((id = Strings::ToInt(sep->arg[2])) <= 0)) { - c->Message( - Chat::White, "Usage: #object Delete (ObjectID) -- NOTE: Object deletions are permanent and " - "cannot be undone!" - ); - return; - } - - o = entity_list.FindObject(id); - - if (o) { - // Object found in zone. - - app = new EQApplicationPacket(); - o->CreateDeSpawnPacket(app); - entity_list.QueueClients(nullptr, app); - entity_list.RemoveObject(o->GetID()); - safe_delete(app); - - // Verifying ZoneID and Version in case someone else ended up adding an object with our ID - // from a different zone/version. Don't want to delete someone else's work. - std::string query = StringFormat( - "DELETE FROM object " - "WHERE id = %u AND zoneid = %u " - "AND version = %u LIMIT 1", - id, zone->GetZoneID(), zone->GetInstanceVersion()); - auto results = content_db.QueryDatabase(query); - - c->Message(Chat::White, "Object %u deleted", id); - return; - } - - // Object not found in zone. - std::string query = StringFormat( - "SELECT type FROM object " - "WHERE id = %u AND zoneid = %u " - "AND version = %u LIMIT 1", - id, zone->GetZoneID(), zone->GetInstanceVersion()); - auto results = content_db.QueryDatabase(query); - if (!results.Success()) { - return; - } - - if (results.RowCount() == 0) { - c->Message(Chat::White, "ERROR: Object %u not found in this zone or instance!", id); - return; - } - - auto row = results.begin(); - - switch (Strings::ToInt(row[0])) { - case 0: // Static Object - query = StringFormat( - "DELETE FROM object WHERE id = %u " - "AND zoneid = %u AND version = %u LIMIT 1", - id, zone->GetZoneID(), zone->GetInstanceVersion()); - results = content_db.QueryDatabase(query); - - c->Message( - Chat::White, "Object %u deleted. NOTE: This static object will remain for anyone currently in " - "the zone until they next zone out and in.", - id - ); - return; - - case 1: // Temporary Spawn - c->Message( - Chat::White, "ERROR: Object %u is a temporarily spawned ground spawn or dropped item, which " - "is not supported with #object. See the 'ground_spawns' table in the database.", - id - ); - return; - } - - return; - } - - if (strcasecmp(sep->arg[1], "undo") == 0) { - // Insufficient or invalid arguments - if ((sep->argnum < 2) || ((id = Strings::ToInt(sep->arg[2])) == 0)) { - c->Message( - Chat::White, "Usage: #object Undo (ObjectID) -- Reload object from database, undoing any " - "changes you have made" - ); - return; - } - - o = entity_list.FindObject(id); - - if (!o) { - c->Message( - Chat::White, "ERROR: Object %u not found in zone in a manipulable form. No changes to undo.", - id - ); - return; - } - - if (o->GetType() == OT_DROPPEDITEM) { - c->Message( - Chat::White, "ERROR: Object %u is a temporary spawned item and cannot be manipulated with " - "#object. See the 'ground_spawns' table in the database.", - id - ); - return; - } - - // Despawn current item for reloading from database - app = new EQApplicationPacket(); - o->CreateDeSpawnPacket(app); - entity_list.QueueClients(0, app); - entity_list.RemoveObject(o->GetID()); - safe_delete(app); - - std::string query = StringFormat( - "SELECT xpos, ypos, zpos, " - "heading, objectname, type, icon, " - "unknown08, unknown10, unknown20 " - "FROM object WHERE id = %u", - id - ); - auto results = content_db.QueryDatabase(query); - if (!results.Success() || results.RowCount() == 0) { - c->Message(Chat::White, "Database Error: %s", results.ErrorMessage().c_str()); - return; - } - - memset(&od, 0, sizeof(od)); - - auto row = results.begin(); - - od.x = Strings::ToFloat(row[0]); - od.y = Strings::ToFloat(row[1]); - od.z = Strings::ToFloat(row[2]); - od.heading = Strings::ToFloat(row[3]); - strn0cpy(od.object_name, row[4], sizeof(od.object_name)); - od.object_type = Strings::ToInt(row[5]); - icon = Strings::ToInt(row[6]); - od.size = Strings::ToInt(row[7]); - od.solidtype = Strings::ToInt(row[8]); - od.unknown020 = Strings::ToInt(row[9]); - - if (od.object_type == 0) { - od.object_type = staticType; - } - - o = new Object(id, od.object_type, icon, od, nullptr); - entity_list.AddObject(o, true); - - c->Message(Chat::White, "Object %u reloaded from database.", id); - return; - } - - c->Message(Chat::White, usage_string); + ObjectManipulation::CommandHandler(c, sep); } - diff --git a/zone/gm_commands/object_manipulation.cpp b/zone/gm_commands/object_manipulation.cpp new file mode 100644 index 0000000000..9f9063298e --- /dev/null +++ b/zone/gm_commands/object_manipulation.cpp @@ -0,0 +1,1322 @@ +#include "object_manipulation.h" +#include "../doors.h" +#include "../object.h" +#include "../../common/misc_functions.h" +#include "../../common/strings.h" +#include "../../common/repositories/object_repository.h" + +#define MAX_CLIENT_MESSAGE_LENGTH 2000 + +void ObjectManipulation::CommandHandler(Client *c, const Seperator *sep) +{ + const int arguments = sep->argnum; + if (!arguments) { + ObjectManipulation::SendSubcommands(c); + return; + } + + const bool is_add = !strcasecmp(sep->arg[1], "add"); + const bool is_delete = !strcasecmp(sep->arg[1], "delete"); + const bool is_edit = !strcasecmp(sep->arg[1], "edit"); + const bool is_icon = !strcasecmp(sep->arg[1], "icon"); + const bool is_incline = !strcasecmp(sep->arg[1], "incline"); + const bool is_model = !strcasecmp(sep->arg[1], "model"); + const bool is_move = !strcasecmp(sep->arg[1], "move"); + const bool is_rotate = !strcasecmp(sep->arg[1], "rotate"); + const bool is_save = !strcasecmp(sep->arg[1], "save"); + const bool is_size = !strcasecmp(sep->arg[1], "size"); + const bool is_solid_type = !strcasecmp(sep->arg[1], "solid_type"); + const bool is_type = !strcasecmp(sep->arg[1], "type"); + const bool is_undo = !strcasecmp(sep->arg[1], "undo"); + + if ( + !is_add && + !is_delete && + !is_edit && + !is_icon && + !is_incline && + !is_model && + !is_move && + !is_rotate && + !is_save && + !is_size && + !is_solid_type && + !is_type && + !is_undo + ) { + ObjectManipulation::SendSubcommands(c); + return; + } + + if (is_add) { + if (arguments < 2) { + c->Message( + Chat::White, + "Usage: #object add [Type] [Model] [Icon] [Size] [Solid Type] [Incline] | Add an object" + ); + c->Message( + Chat::White, + "Note: Model must start with a letter, max length 16. Solid Types | 0 (Solid), 1 (Sometimes Non-Solid)" + ); + return; + } + + Object_Struct od; + + memset(&od, 0, sizeof(od)); + + const uint32 type = Strings::ToUnsignedInt(sep->arg[2]) == ObjectTypes::StaticLocked ? ObjectTypes::StaticUnlocked : Strings::ToUnsignedInt(sep->arg[2]); + if (type == ObjectTypes::StaticLocked) { + c->Message( + Chat::White, + "Note: Object Type 0 will act like a tradeskill container and will not reflect " + "size, solidtype, or incline values until you commit with '#object save', after " + "which it will be unchangeable until you use '#object edit' and zone back in." + ); + } else if (type == ObjectTypes::Temporary) { + c->Message( + Chat::White, + "Note: Object Type 1 is used for temporarily spawned ground spawns and dropped " + "items, which are not supported with #object. See the 'ground_spawns' table in " + "the database." + ); + return; + } + + std::string name = sep->arg[3]; + name = Strings::ToUpper(Strings::Replace(name, "'", "")); + + if (name[0] < 'A' || name[0] > 'Z') { + c->Message(Chat::White, "Model name must start with a letter."); + return; + } + + const uint32 icon = Strings::ToUnsignedInt(sep->arg[4]); + const float size = Strings::ToFloat(sep->arg[5]); + const uint16 solid_type = static_cast(Strings::ToUnsignedInt(sep->arg[6])); + const uint32 incline = Strings::ToUnsignedInt(sep->arg[7]); + + const uint32 zone_id = zone->GetZoneID(); + const uint16 instance_version = zone->GetInstanceVersion(); + + od.incline = incline; + od.object_type = type; + od.size = size; + od.solid_type = solid_type; + od.x = c->GetX(); + od.y = c->GetY(); + od.z = c->GetZ(); + od.heading = c->GetHeading(); + od.zone_id = zone_id; + od.zone_instance = instance_version; + + strn0cpy(od.object_name, name.c_str(), sizeof(od.object_name)); + + const auto &l = ObjectRepository::GetWhere( + content_db, + fmt::format( + SQL( + zoneid = {} AND (version = {} OR version = -1) AND + xpos BETWEEN {:.2f} AND {:.2f} AND + ypos BETWEEN {:.2f} AND {:.2f} AND + zpos BETWEEN {:.2f} AND {:.2f} + ), + zone_id, + instance_version, + od.x - 0.2f, + od.x + 0.2f, + od.y - 0.2f, + od.y + 0.2f, + od.z - 0.2f, + od.z + 0.2f + ) + ); + + const bool object_found = l.empty(); + + if (object_found) { + c->Message(Chat::White, "An object already exists at this location."); + return; + } + + const uint32 object_id = (ObjectRepository::GetMaxId(content_db) + 1); + + Object *o = new Object( + object_id, + od.object_type, + icon, + od, + nullptr + ); + + entity_list.AddObject(o, true); + + const float position_offset = 10.0f * sin(c->GetHeading() / 256.0f * 3.14159265f); + c->MovePC( + c->GetX() - position_offset, + c->GetY() - position_offset, + c->GetZ(), + c->GetHeading() + ); + + const auto &save_saylink = Saylink::Silent( + fmt::format( + "#object save {}", + object_id + ), + "save" + ); + + c->Message( + Chat::White, + fmt::format( + "Spawned object ID {} at {:.2f}, {:.2f}, {:.2f}, {:.2f}. You can {} it if you'd like.", + object_id, + od.x, + od.y, + od.z, + od.heading, + save_saylink + ).c_str() + ); + + c->SetObjectToolEntityId(o->GetID()); + } else if (is_delete) { + if (!c->GetObjectToolEntityId()) { + c->Message(Chat::White, "You do not have a selected object."); + return; + } + + Object *o = entity_list.GetObjectByID(c->GetObjectToolEntityId()); + + const uint32 object_id = o->GetDBID(); + + if (o) { + auto app = new EQApplicationPacket(); + o->CreateDeSpawnPacket(app); + entity_list.QueueClients(nullptr, app); + entity_list.RemoveObject(o->GetID()); + safe_delete(app); + + const int deleted_object = ObjectRepository::DeleteWhere( + content_db, + fmt::format( + "id = {} AND zoneid = {} AND version = {}", + object_id, + zone->GetZoneID(), + zone->GetInstanceVersion() + ) + ); + + if (deleted_object) { + c->Message( + Chat::White, + fmt::format( + "Successfully deleted Object ID {}.", + object_id + ).c_str() + ); + } else { + c->Message( + Chat::White, + fmt::format( + "Failed to delete Object ID {}.", + object_id + ).c_str() + ); + } + + return; + } + + const auto &e = ObjectRepository::GetWhere( + content_db, + fmt::format( + "id = {} AND zoneid = {} AND version = {} LIMIT 1", + object_id, + zone->GetZoneID(), + zone->GetInstanceVersion() + ) + ); + + if (e[0].type == ObjectTypes::Temporary) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} is a temporarily spawned ground spawn or dropped item, which is not supported with #object. See the 'ground_spawns' table in the database.", + object_id + ).c_str() + ); + return; + } + + const int deleted_object = ObjectRepository::DeleteWhere( + content_db, + fmt::format( + "id = {} AND zoneid = {} AND version = {}", + object_id, + zone->GetZoneID(), + zone->GetInstanceVersion() + ) + ); + + if (deleted_object) { + c->Message( + Chat::White, + fmt::format( + "Successfully deleted Object ID {}.", + object_id + ).c_str() + ); + } else { + c->Message( + Chat::White, + fmt::format( + "Failed to delete Object ID {}.", + object_id + ).c_str() + ); + } + } else if (is_edit) { + Object *o = entity_list.GetObjectByID(c->GetObjectToolEntityId()); + + const uint32 object_id = o->GetDBID(); + + c->Message( + Chat::White, + fmt::format( + "Object Selected ID [{}] Database ID [{}]", + c->GetObjectToolEntityId(), + object_id + ).c_str() + ); + + 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 rotate_options_positive; + std::vector rotate_options_negative; + std::vector size_options_positive; + std::vector size_options_negative; + + std::vector xyz_values = { + "0.1", "1", "5", "10", "25", "50", "100" + }; + + for (const auto &v: xyz_values) { + const float current_x = o->GetX(); + const float current_y = o->GetY(); + const float current_z = o->GetZ(); + const float current_heading = o->GetHeadingData(); + + const float new_x = current_x + Strings::ToFloat(v); + const float new_y = current_y + Strings::ToFloat(v); + const float new_z = current_z + Strings::ToFloat(v); + const float new_heading = current_heading + Strings::ToFloat(v); + + move_x_options_positive.emplace_back( + Saylink::Silent( + fmt::format( + "#object move {:.2f} {:.2f} {:.2f}", + new_x, + current_y, + current_z + ), + v + ) + ); + + move_y_options_positive.emplace_back( + Saylink::Silent( + fmt::format( + "#object move {:.2f} {:.2f} {:.2f}", + current_x, + new_y, + current_z + ), + v + ) + ); + + move_z_options_positive.emplace_back( + Saylink::Silent( + fmt::format( + "#object move {:.2f} {:.2f} {:.2f}", + current_x, + current_y, + new_z + ), + v + ) + ); + + rotate_options_positive.emplace_back( + Saylink::Silent( + fmt::format( + "#object rotate {:.2f}", + new_heading + ), + v + ) + ); + } + + for (auto v = xyz_values.rbegin(); v != xyz_values.rend(); ++v) { + const float current_x = o->GetX(); + const float current_y = o->GetY(); + const float current_z = o->GetZ(); + const float current_heading = o->GetHeadingData(); + + const float new_x = current_x - Strings::ToFloat(*v); + const float new_y = current_y - Strings::ToFloat(*v); + const float new_z = current_z - Strings::ToFloat(*v); + const float new_heading = current_heading - Strings::ToFloat(*v); + + move_x_options_negative.emplace_back( + Saylink::Silent( + fmt::format( + "#object move {:.2f} {:.2f} {:.2f}", + new_x, + current_y, + current_z + ), + *v + ) + ); + + move_y_options_negative.emplace_back( + Saylink::Silent( + fmt::format( + "#object move {:.2f} {:.2f} {:.2f}", + current_x, + new_y, + current_z + ), + *v + ) + ); + + move_z_options_negative.emplace_back( + Saylink::Silent( + fmt::format( + "#object move {:.2f} {:.2f} {:.2f}", + current_x, + current_y, + new_z + ), + *v + ) + ); + + rotate_options_negative.emplace_back( + Saylink::Silent( + fmt::format( + "#object rotate {:.2f}", + new_heading + ), + *v + ) + ); + } + + std::vector size_values = { + "1", "5", "10", "25", "50", "100", "1000" + }; + + for (const auto &v: size_values) { + const float current_size = o->GetSize(); + + const float new_size = current_size + Strings::ToFloat(v); + + size_options_positive.emplace_back( + Saylink::Silent(fmt::format("#door size {:.2f}", new_size), v) + ); + } + + for (auto v = size_values.rbegin(); v != size_values.rend(); ++v) { + const float current_size = o->GetSize(); + + const float new_size = current_size - Strings::ToFloat(*v); + + size_options_negative.emplace_back( + Saylink::Silent(fmt::format("#door edit {:.2f}", new_size, *v), *v) + ); + } + + c->Message( + Chat::White, + fmt::format( + "Name [{}] [{}] [{}] [{}]", + o->GetModelName(), + Saylink::Silent("#object save", "Save"), + Saylink::Silent("#object delete", "Delete"), + Saylink::Silent("#object undo", "Undo") + ).c_str() + ); + + c->Message( + Chat::White, + fmt::format( + "[{}] - [X] + [{}]", + Strings::Implode(" | ", move_x_options_negative), + Strings::Implode(" | ", move_x_options_positive) + ).c_str() + ); + + c->Message( + Chat::White, + fmt::format( + "[{}] - [Y] + [{}]", + Strings::Implode(" | ", move_y_options_negative), + Strings::Implode(" | ", move_y_options_positive) + ).c_str() + ); + + c->Message( + Chat::White, + fmt::format( + "[{}] - [Z] + [{}]", + Strings::Implode(" | ", move_z_options_negative), + Strings::Implode(" | ", move_z_options_positive) + ).c_str() + ); + + c->Message( + Chat::White, + fmt::format( + "[{}] - [H] + [{}]", + Strings::Implode(" | ", rotate_options_negative), + Strings::Implode(" | ", rotate_options_positive) + ).c_str() + ); + + c->Message( + Chat::White, + fmt::format( + "[{}] - [Size] + [{}]", + Strings::Implode(" | ", size_options_negative), + Strings::Implode(" | ", size_options_positive) + ).c_str() + ); + + return; + } else if ( + is_icon || + is_incline || + is_model || + is_size || + is_solid_type || + is_type + ) { + if (!c->GetObjectToolEntityId()) { + c->Message(Chat::White, "You do not have a selected object."); + return; + } + + Object *o = entity_list.GetObjectByID(c->GetObjectToolEntityId()); + + const uint32 object_id = o->GetDBID(); + + Object_Struct od; + + if (!o) { + auto e = ObjectRepository::FindOne(content_db, object_id); + if (!e.id) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} does not exist.", + object_id + ).c_str() + ); + return; + } + + if (e.zoneid != zone->GetZoneID()) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} is not a part of this zone.", + object_id + ).c_str() + ); + return; + } + + if (e.version != zone->GetInstanceVersion()) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} is not a part of this instance version.", + object_id + ).c_str() + ); + return; + } + + memset(&od, 0, sizeof(od)); + + od.zone_id = e.zoneid; + od.zone_instance = e.version; + od.object_type = e.type; + + if (od.object_type == ObjectTypes::StaticLocked) { + e.type = ObjectTypes::StaticUnlocked; + const int updated = ObjectRepository::UpdateOne(content_db, e); + if (updated) { + const std::string &reload_saylink = Saylink::Silent( + "#reload objects", + "reload" + ); + + const auto &save_saylink = Saylink::Silent( + fmt::format( + "#object save {}", + object_id + ), + "save" + ); + + c->Message( + Chat::White, + fmt::format( + "Object ID {} unlocked, you must {} to make changes then you can {}.", + object_id, + reload_saylink, + save_saylink + ).c_str() + ); + return; + } + } else if (od.object_type == ObjectTypes::Temporary) { + c->Message( + Chat::White, + "Note: Object Type 1 is used for temporarily spawned ground spawns and dropped " + "items, which are not supported with #object. See the 'ground_spawns' table in " + "the database." + ); + return; + } + } + + uint32 icon = o->GetIcon(); + o->GetObjectData(&od); + + if (is_icon) { + if ( + od.object_type <= ObjectTypes::Temporary || + od.object_type == ObjectTypes::StaticUnlocked + ) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} is not a Tradeskill Object and does not support the 'icon' property.", + object_id + ).c_str() + ); + return; + } + + if (!Strings::IsNumber(sep->arg[2])) { + c->Message(Chat::White, "Invalid icon specified. Please enter a valid icon."); + return; + } + + icon = Strings::ToUnsignedInt(sep->arg[2]); + + o->SetIcon(icon); + + c->Message( + Chat::White, + fmt::format( + "Object ID {} icon set to {}.", + object_id, + icon + ).c_str() + ); + } else if (is_incline) { + if (od.object_type != ObjectTypes::StaticUnlocked) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} is not a static object and does not support the 'incline' property.", + object_id + ).c_str() + ); + return; + } + + if (!Strings::IsNumber(sep->arg[2])) { + c->Message(Chat::White, "Invalid incline specified. Please enter a valid incline."); + return; + } + + od.incline = Strings::ToUnsignedInt(sep->arg[2]); + o->SetObjectData(&od); + + const auto &save_saylink = Saylink::Silent( + fmt::format( + "#object save {}", + object_id + ), + "save" + ); + + c->Message( + Chat::White, + fmt::format( + "Object ID {} set to {} incline. You will need to {} to retain this change.", + object_id, + od.incline, + save_saylink + ).c_str() + ); + } else if (is_model) { + const std::string &object_name = sep->argplus[2]; + + if (object_name[0] < 'A' || object_name[0] > 'Z') { + c->Message(Chat::White, "Model names must begin with a letter."); + return; + } + + strn0cpy(od.object_name, object_name.c_str(), sizeof(od.object_name)); + + c->Message( + Chat::White, + fmt::format( + "Object ID {} is now using model '{}'", + object_id, + object_name + ).c_str() + ); + } else if (is_size) { + if (od.object_type != ObjectTypes::StaticUnlocked) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} is not a static object and does not support the 'size' property.", + object_id + ).c_str() + ); + return; + } + + if (!Strings::IsNumber(sep->arg[2])) { + c->Message(Chat::White, "Invalid size specified. Please provide a valid size."); + return; + } + + od.size = Strings::ToFloat(sep->arg[2]); + o->SetObjectData(&od); + + if (od.size == 0.0f) { + od.size = 100.0f; + } + + const auto &save_saylink = Saylink::Silent( + fmt::format( + "#object save {}", + object_id + ), + "save" + ); + + c->Message( + Chat::White, + fmt::format( + "Object ID {} set to {:.2f} size. You will need to {} to retain this change.", + object_id, + od.size, + save_saylink + ).c_str() + ); + } else if (is_solid_type) { + if (od.object_type != ObjectTypes::StaticUnlocked) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} is not a static object and does not support the 'solid_type' property.", + object_id + ).c_str() + ); + return; + } + + if (!Strings::IsNumber(sep->arg[2])) { + c->Message(Chat::White, "Invalid solid type specified. Please provide a valid solid type."); + return; + } + + od.solid_type = static_cast(Strings::ToUnsignedInt(sep->arg[2])); + o->SetObjectData(&od); + + const auto &save_saylink = Saylink::Silent( + fmt::format( + "#object save {}", + object_id + ), + "save" + ); + + c->Message( + Chat::White, + fmt::format( + "Object ID {} set to solid type {}. You will need to {} to retain this change.", + object_id, + od.solid_type, + save_saylink + ).c_str() + ); + } else if (is_type) { + if (!Strings::IsNumber(sep->arg[2])) { + c->Message(Chat::White, "Invalid type specified. Please enter a valid type."); + return; + } + + od.object_type = Strings::ToUnsignedInt(sep->arg[2]); + + if (od.object_type == ObjectTypes::StaticLocked) { + const auto &save_saylink = Saylink::Silent( + fmt::format( + "#object save {}", + object_id + ), + "save" + ); + + c->Message( + Chat::White, + fmt::format( + "Note: Static Objects will not reflect property changes until you {}.", + save_saylink + ).c_str() + ); + return; + } else if (od.object_type == ObjectTypes::Temporary) { + c->Message( + Chat::White, + "Note: Object Type 1 is used for temporarily spawned ground spawns and dropped " + "items, which are not supported with #object. See the 'ground_spawns' table in " + "the database." + ); + return; + } + + o->SetType(od.object_type); + + c->Message( + Chat::White, + fmt::format( + "Object ID {} set to type {}.", + object_id, + od.object_type + ).c_str() + ); + } + + auto app = new EQApplicationPacket(); + o->CreateDeSpawnPacket(app); + entity_list.QueueClients(0, app); + safe_delete(app); + + app = new EQApplicationPacket(); + o->CreateSpawnPacket(app); + entity_list.QueueClients(0, app); + safe_delete(app); + return; + } else if (is_move) { + if (!c->GetObjectToolEntityId()) { + c->Message(Chat::White, "You do not have a selected object."); + return; + } + + if (arguments < 2) { + c->Message(Chat::White, "Usage: #object move [X] [Y] [Z] | Move the selected object"); + return; + } + + Object_Struct od; + + Object *o = entity_list.GetObjectByID(c->GetObjectToolEntityId()); + + const uint32 object_id = o->GetDBID(); + + if (!o) { + const auto &e = ObjectRepository::FindOne(content_db, object_id); + + if (!e.id) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} does not exist.", + object_id + ).c_str() + ); + return; + } + + od.zone_id = e.zoneid; + od.zone_instance = e.version; + od.object_type = e.type; + + if (e.zoneid != zone->GetZoneID()) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} is not a part of this zone.", + object_id + ).c_str() + ); + return; + } + + if (e.version != zone->GetInstanceVersion()) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} is not a part of this instance version.", + object_id + ).c_str() + ); + return; + } + + if (od.object_type == ObjectTypes::StaticLocked) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} is not unlocked for editing. Use '#object edit' to unlock it.", + object_id + ).c_str() + ); + return; + } else if (od.object_type == ObjectTypes::Temporary) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} is a temporarily spawned ground spawn or dropped item, which is not supported with #object. See the 'ground_spawns' table in the database.", + object_id + ).c_str() + ); + return; + } else if (od.object_type == ObjectTypes::StaticUnlocked) { + const std::string &reload_saylink = Saylink::Silent( + "#reload objects", + "reload" + ); + + const std::string &save_saylink = Saylink::Silent( + fmt::format( + "#object save {}", + object_id + ), + "save" + ); + + c->Message( + Chat::White, + fmt::format( + "Object ID {} has been unlocked for editing, you must {} to make changes then you can {}.", + object_id, + reload_saylink, + save_saylink + ).c_str() + ); + } else { + c->Message( + Chat::White, + fmt::format( + "Object ID {} could not be located.", + object_id + ).c_str() + ); + return; + } + } + + const float x = Strings::IsFloat(sep->arg[2]) ? Strings::ToFloat(sep->arg[2]) : 0.0f; + const float y = arguments >= 3 ? Strings::ToFloat(sep->arg[3]) : 0.0f; + const float z = arguments >= 4 ? Strings::ToFloat(sep->arg[4]) : 0.0f; + + od.x = x; + + if (arguments >= 3) { + od.y = y; + } else { + o->GetLocation(nullptr, &od.y, nullptr); + } + + if (arguments >= 4) { + od.z = z; + } else { + o->GetLocation(nullptr, nullptr, &od.z); + } + + o->SetLocation(od.x, od.y, od.z); + + auto app = new EQApplicationPacket(); + o->CreateDeSpawnPacket(app); + entity_list.QueueClients(0, app); + safe_delete(app); + + app = new EQApplicationPacket(); + o->CreateSpawnPacket(app); + entity_list.QueueClients(0, app); + safe_delete(app); + return; + } else if (is_rotate) { + if (!c->GetObjectToolEntityId()) { + c->Message(Chat::White, "You do not have a selected object."); + return; + } + + if (!sep->IsNumber(2)) { + c->Message(Chat::White, "Usage: #object rotate [Heading] | Rotate the selected object"); + return; + } + + Object *o = entity_list.GetObjectByID(c->GetObjectToolEntityId()); + + const uint32 object_id = o->GetDBID(); + + const float heading = Strings::ToFloat(sep->arg[2]); + + o->SetHeading(heading); + + auto app = new EQApplicationPacket(); + o->CreateDeSpawnPacket(app); + entity_list.QueueClients(0, app); + safe_delete(app); + + app = new EQApplicationPacket(); + o->CreateSpawnPacket(app); + entity_list.QueueClients(0, app); + safe_delete(app); + return; + } else if (is_save) { + if (!c->GetObjectToolEntityId()) { + c->Message(Chat::White, "You do not have a selected object."); + return; + } + + Object_Struct od; + + Object *o = entity_list.GetObjectByID(c->GetObjectToolEntityId()); + + uint32 object_id = o->GetDBID(); + + od.zone_id = 0; + od.zone_instance = 0; + od.object_type = 0; + + bool is_new = true; + + const auto &e = ObjectRepository::FindOne(content_db, object_id); + if (e.id) { + od.zone_id = e.zoneid; + od.zone_instance = e.version; + od.object_type = e.type; + + is_new = false; + } + + if (od.zone_id && od.zone_id != zone->GetZoneID()) { + object_id = 0; + } + + if (object_id > 0 && od.zone_instance != zone->GetInstanceVersion()) { + object_id = 0; + } + + is_new |= (object_id == 0); + + o->GetObjectData(&od); + + od.object_type = o->GetType(); + + const uint32 icon = o->GetIcon(); + + if (od.object_type == ObjectTypes::StaticUnlocked) { + od.object_type = ObjectTypes::StaticLocked; + } + + int updated = 0; + + if (!is_new) { + auto e = ObjectRepository::FindOne(content_db, object_id); + + e.xpos = od.x; + e.ypos = od.y; + e.zpos = od.z; + e.heading = od.heading; + e.objectname = od.object_name; + e.type = od.object_type; + e.icon = icon; + e.size_percentage = od.size; + e.solid_type = od.solid_type; + e.incline = od.incline; + + updated = ObjectRepository::UpdateOne(content_db, e); + } else if (!object_id) { + auto e = ObjectRepository::NewEntity(); + + e.xpos = od.x; + e.ypos = od.y; + e.zpos = od.z; + e.heading = od.heading; + e.objectname = od.object_name; + e.type = od.object_type; + e.icon = icon; + e.size_percentage = od.size; + e.solid_type = od.solid_type; + e.incline = od.incline; + e.zoneid = zone->GetZoneID(); + e.version = zone->GetInstanceVersion(); + + e = ObjectRepository::InsertOne(content_db, e); + updated = e.id ? 2 : 0; + object_id = e.id; + } else { + auto e = ObjectRepository::NewEntity(); + + e.id = object_id; + e.xpos = od.x; + e.ypos = od.y; + e.zpos = od.z; + e.heading = od.heading; + e.objectname = od.object_name; + e.type = od.object_type; + e.icon = icon; + e.size_percentage = od.size; + e.solid_type = od.solid_type; + e.incline = od.incline; + e.zoneid = zone->GetZoneID(); + e.version = zone->GetInstanceVersion(); + + updated = ObjectRepository::InsertOne(content_db, e).id ? 1 : 0; + } + + if (!updated) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} could not be saved.", + object_id + ).c_str() + ); + return; + } + + if (is_new) { + if (updated == 1) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} has been saved.", + object_id + ).c_str() + ); + } else if (updated == 2) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} has been created.", + object_id + ).c_str() + ); + } + return; + } + + c->Message( + Chat::White, + fmt::format( + "Object ID {} has been saved.", + object_id + ).c_str() + ); + + if (od.object_type == ObjectTypes::StaticLocked) { + auto app = new EQApplicationPacket(); + o->CreateDeSpawnPacket(app); + entity_list.QueueClients(0, app); + safe_delete(app); + + entity_list.RemoveObject(o->GetID()); + + auto door = DoorsRepository::NewEntity(); + + door.zone = zone->GetShortName(); + + door.id = 1000000000 + object_id; // Out of range of normal use for doors.id + door.doorid = -1; // Client doesn't care if these are all the same door_id + door.pos_x = od.x; + door.pos_y = od.y; + door.pos_z = od.z; + door.heading = od.heading; + + door.name = Strings::Replace(od.object_name, "_ACTORDEF", ""); + + door.dest_zone = "NONE"; + + if ((door.size = od.size) == 0) { + door.size = 100; + } + + door.opentype = od.solid_type; + + switch (door.opentype) { + case 0: + door.opentype = 31; + break; + case 1: + door.opentype = 9; + break; + } + + door.incline = od.incline; // unknown20 = optional incline value + door.client_version_mask = 0xFFFFFFFF; + + Doors *doors = new Doors(door); + + entity_list.AddDoor(doors); + + app = new EQApplicationPacket(OP_SpawnDoor, sizeof(Door_Struct)); + auto ds = (Door_Struct *) app->pBuffer; + + memset(ds, 0, sizeof(Door_Struct)); + memcpy(ds->name, door.name.c_str(), 32); + + ds->xPos = door.pos_x; + ds->yPos = door.pos_y; + ds->zPos = door.pos_z; + ds->heading = door.heading; + ds->incline = door.incline; + ds->size = door.size; + ds->doorId = door.doorid; + ds->opentype = door.opentype; + ds->unknown0052[9] = 1; // *ptr-1 and *ptr-3 from EntityList::MakeDoorSpawnPacket() + ds->unknown0052[11] = 1; + + entity_list.QueueClients(0, app); + safe_delete(app); + + c->Message( + Chat::White, + fmt::format( + "Object ID {} is now a static object and cannot be edited. Use '#object edit' to edit it again.", + object_id + ).c_str() + ); + } + } else if (is_undo) { + if (!c->GetObjectToolEntityId()) { + c->Message(Chat::White, "You do not have a selected object."); + return; + } + + Object *o = entity_list.GetObjectByID(c->GetObjectToolEntityId()); + + const uint32 object_id = o->GetDBID(); + + if (o->GetType() == ObjectTypes::Temporary) { + c->Message( + Chat::White, + "Note: Object Type 1 is used for temporarily spawned ground spawns and dropped " + "items, which are not supported with #object. See the 'ground_spawns' table in " + "the database." + ); + return; + } + + auto app = new EQApplicationPacket(); + o->CreateDeSpawnPacket(app); + entity_list.QueueClients(0, app); + entity_list.RemoveObject(o->GetID()); + safe_delete(app); + + const auto &e = ObjectRepository::FindOne(content_db, object_id); + + if (!e.id) { + c->Message( + Chat::White, + fmt::format( + "Object ID {} does not exist.", + object_id + ).c_str() + ); + return; + } + + Object_Struct od; + + memset(&od, 0, sizeof(od)); + + const uint32 icon = e.icon; + + od.x = e.xpos; + od.y = e.ypos; + od.z = e.zpos; + od.heading = e.heading; + od.object_type = e.type; + od.size = e.size_percentage; + od.solid_type = e.solid_type; + od.incline = e.incline; + + strn0cpy(od.object_name, e.objectname.c_str(), sizeof(od.object_name)); + + if (od.object_type == ObjectTypes::StaticUnlocked) { + od.object_type = ObjectTypes::StaticLocked; + } + + o = new Object(object_id, od.object_type, icon, od, nullptr); + entity_list.AddObject(o, true); + + c->Message( + Chat::White, + fmt::format( + "Object ID {} reloaded from database.", + object_id + ).c_str() + ); + } +} + +void ObjectManipulation::CommandHeader(Client *c) +{ + c->Message(Chat::White, "------------------------------------------------"); + c->Message(Chat::White, "# Object Commands"); + c->Message(Chat::White, "------------------------------------------------"); +} + +void ObjectManipulation::SendSubcommands(Client *c) +{ + ObjectManipulation::CommandHeader(c); + c->Message( + Chat::White, + "Usage: #object add [Type] [Model] [Icon] [Size] [Solid Type] [Incline] | Add an object" + ); + c->Message( + Chat::White, + "Note: Model must start with a letter, max length 16. Solid Types | 0 (Solid), 1 (Sometimes Non-Solid)" + ); + c->Message(Chat::White, "Note: Only objects saved in the database can be copied to another instance"); + c->Message(Chat::White, "Usage: #object delete | Delete the selected object"); + c->Message(Chat::White, "Usage: #object edit | Edit the selected object"); + c->Message(Chat::White, "Note: Static Object (Type 0) Properties: model, type, size, solid_type, incline"); + c->Message(Chat::White, "Note: Tradeskill Object (Type 2+) Properties: model, type, icon"); + c->Message(Chat::White, "Usage: #object icon [Icon] | Edit the selected object's icon"); + c->Message(Chat::White, "Usage: #object incline [Incline] | Edit the selected object's incline"); + c->Message(Chat::White, "Usage: #object model [Model] | Edit the selected object's model"); + c->Message(Chat::White, "Usage: #object move [0|X] [Y] [Z] [H] | Move the selected object"); + c->Message(Chat::White, "Note: Using 0 for X moves the object to your position, heading is optional"); + c->Message(Chat::White, "Usage: #object rotate [Heading] | Rotate the selected object"); + c->Message(Chat::White, "Usage: #object save | Save the selected object"); + c->Message(Chat::White, "Usage: #object size [Size] | Edit the selected object's size"); + c->Message(Chat::White, "Usage: #object solid_type [Solid Type] | Edit the selected object's solid type"); + c->Message(Chat::White, "Usage: #object type [Type] | Edit the selected object's type"); + c->Message(Chat::White, "Usage: #object undo | Reload the selected object from the database"); +} diff --git a/zone/gm_commands/object_manipulation.h b/zone/gm_commands/object_manipulation.h new file mode 100644 index 0000000000..57503b0144 --- /dev/null +++ b/zone/gm_commands/object_manipulation.h @@ -0,0 +1,15 @@ +#ifndef EQEMU_OBJECT_MANIPULATION_H +#define EQEMU_OBJECT_MANIPULATION_H + +#include "../client.h" + +class ObjectManipulation { + +public: + static void CommandHandler(Client *c, const Seperator *sep); + static void CommandHeader(Client *c); + static void SendSubcommands(Client *c); +}; + + +#endif //EQEMU_OBJECT_MANIPULATION_H diff --git a/zone/object.cpp b/zone/object.cpp index 9f5bf79659..cfb42ddad0 100644 --- a/zone/object.cpp +++ b/zone/object.cpp @@ -40,19 +40,24 @@ extern EntityList entity_list; extern WorldServer worldserver; // Loading object from database -Object::Object(uint32 id, uint32 type, uint32 icon, const Object_Struct& object, const EQ::ItemInstance* inst) - : respawn_timer(0), decay_timer(300000) +Object::Object( + uint32 id, + uint32 type, + uint32 icon, + const Object_Struct &object, + const EQ::ItemInstance *inst +) : respawn_timer(0), decay_timer(300000) { - - user = nullptr; + user = nullptr; last_user = nullptr; // Initialize members - m_id = id; - m_type = type; - m_icon = icon; - m_inst = nullptr; - m_ground_spawn=false; + m_id = id; + m_type = type; + m_icon = icon; + m_inst = nullptr; + m_ground_spawn = false; + // Copy object data memcpy(&m_data, &object, sizeof(Object_Struct)); if (inst) { @@ -61,13 +66,14 @@ Object::Object(uint32 id, uint32 type, uint32 icon, const Object_Struct& object, } else { decay_timer.Disable(); } + respawn_timer.Disable(); // Set drop_id to zero - it will be set when added to zone with SetID() m_data.drop_id = 0; - m_data.size = object.size; - m_data.tilt_x = object.tilt_x; - m_data.tilt_y = object.tilt_y; + m_data.size = object.size; + m_data.tilt_x = object.tilt_x; + m_data.tilt_y = object.tilt_y; FixZ(); } @@ -85,7 +91,7 @@ Object::Object(const EQ::ItemInstance* inst, char* name,float max_x,float min_x, m_min_y=min_y; m_id = 0; m_inst = (inst) ? inst->Clone() : nullptr; - m_type = OT_DROPPEDITEM; + m_type = ObjectTypes::Temporary; m_icon = 0; m_ground_spawn = true; decay_timer.Disable(); @@ -116,7 +122,7 @@ Object::Object(Client* client, const EQ::ItemInstance* inst) // Initialize members m_id = 0; m_inst = (inst) ? inst->Clone() : nullptr; - m_type = OT_DROPPEDITEM; + m_type = ObjectTypes::Temporary; m_icon = 0; m_ground_spawn = false; // Set as much struct data as we can @@ -179,7 +185,7 @@ Object::Object(const EQ::ItemInstance *inst, float x, float y, float z, float he // Initialize members m_id = 0; m_inst = (inst) ? inst->Clone() : nullptr; - m_type = OT_DROPPEDITEM; + m_type = ObjectTypes::Temporary; m_icon = 0; m_ground_spawn = false; // Set as much struct data as we can @@ -436,7 +442,7 @@ void Object::CreateDeSpawnPacket(EQApplicationPacket* app) } bool Object::Process(){ - if(m_type == OT_DROPPEDITEM && decay_timer.Enabled() && decay_timer.Check()) { + if(m_type == ObjectTypes::Temporary && decay_timer.Enabled() && decay_timer.Check()) { // Send click to all clients (removes entity on client) auto outapp = new EQApplicationPacket(OP_ClickObject, sizeof(ClickObject_Struct)); ClickObject_Struct* click_object = (ClickObject_Struct*)outapp->pBuffer; @@ -497,7 +503,7 @@ bool Object::HandleClick(Client* sender, const ClickObject_Struct* click_object) if(m_ground_spawn) {//This is a Cool Groundspawn respawn_timer.Start(); } - if (m_type == OT_DROPPEDITEM) { + if (m_type == ObjectTypes::Temporary) { bool cursordelete = false; bool duplicate_lore = false; if (m_inst && sender) { @@ -969,7 +975,7 @@ void Object::SetSize(float size) void Object::SetSolidType(uint16 solidtype) { - m_data.solidtype = solidtype; + m_data.solid_type = solidtype; auto app = new EQApplicationPacket(); auto app2 = new EQApplicationPacket(); CreateDeSpawnPacket(app); @@ -987,7 +993,7 @@ float Object::GetSize() uint16 Object::GetSolidType() { - return m_data.solidtype; + return m_data.solid_type; } const char* Object::GetModelName() diff --git a/zone/object.h b/zone/object.h index 23791af2c2..7fd605626f 100644 --- a/zone/object.h +++ b/zone/object.h @@ -73,8 +73,6 @@ IT10714_ACTORDEF=Augmentation Sealer IT10725_ACTORDEF=Shuriken */ -#define OT_DROPPEDITEM EQ::item::BagTypeLargeBag - // Icon values: //0x0453 a pie //0x0454 cookies? @@ -89,6 +87,51 @@ IT10725_ACTORDEF=Shuriken //0x045D is a hammer //0x045E is a wierd rope shape +enum ObjectTypes { + StaticLocked = 0, + Temporary = 1, + ToolBox = 10, + Research = 11, + Mortar = 12, + SelfDusting = 13, + Baking1 = 14, + Baking2 = 15, + Tailoring = 16, + Forge = 17, + Fletching = 18, + BrewBarrel = 19, + Jewelcrafting = 20, + PotteryWheel = 21, + PotteryKiln = 22, + WizardResearch = 24, + MagicianResearch = 25, + NecromancerResearch = 26, + EnchanterResearch = 27, + Invalid1 = 28, + Invalid2 = 29, + Experimental = 30, + HighElfForge = 31, + DarkElfForge = 32, + OgreForge = 33, + DwarfForge = 34, + GnomeForge = 35, + BarbarianForge = 36, + IksarForge = 38, + HumanForge = 39, + HumanForge2 = 40, + HalflingTailoring = 41, + EruditeTailoring = 42, + WoodElfTailoring = 43, + WoodElfFletching = 44, + IksarPotteryWheel = 45, + TrollForge = 47, + WoodElfForge = 48, + HalflingForge = 49, + EruditeForge = 50, + AugmentationPool = 53, + StaticUnlocked = 255 +}; + class Object: public Entity { public: diff --git a/zone/zone.cpp b/zone/zone.cpp index 3856a4a033..3f0f588269 100644 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -56,6 +56,7 @@ #include "zone_reload.h" #include "../common/repositories/criteria/content_filter_criteria.h" #include "../common/repositories/merchantlist_repository.h" +#include "../common/repositories/object_repository.h" #include "../common/repositories/rule_sets_repository.h" #include "../common/serverinfo.h" @@ -172,101 +173,94 @@ bool Zone::Bootup(uint32 iZoneID, uint32 iInstanceID, bool is_static) { //this really loads the objects into entity_list bool Zone::LoadZoneObjects() { - std::string query = StringFormat( - "SELECT id, zoneid, xpos, ypos, zpos, heading, itemid, charges, objectname, type, icon, " - "unknown08, unknown10, unknown20, unknown24, unknown76, size, tilt_x, tilt_y, display_name " - "FROM object WHERE zoneid = %i AND (version = %u OR version = -1) %s", - zoneid, - instanceversion, - ContentFilterCriteria::apply().c_str() + const auto &l = ObjectRepository::GetWhere( + content_db, + fmt::format( + "zoneid = {} AND (version = {} OR version = -1) {}", + zoneid, + instanceversion, + ContentFilterCriteria::apply() + ) ); - auto results = content_db.QueryDatabase(query); - if (!results.Success()) { - LogError("Error Loading Objects from DB: [{}]", - results.ErrorMessage().c_str()); + if (l.empty()) { + LogError("Error Loading Objects for Zone [{}] Version [{}]", zoneid, instanceversion); return false; } - for (auto row = results.begin(); row != results.end(); ++row) { - if (Strings::ToInt(row[9]) == 0) { - // Type == 0 - Static Object - const char *shortname = ZoneName(Strings::ToInt(row[1]), false); // zoneid -> zone_shortname + for (const auto &e : l) { + if (e.type == ObjectTypes::StaticLocked) { + const std::string &zone_short_name = ZoneName(e.zoneid, false); - if (!shortname) + if (zone_short_name.empty()) { continue; - - // todo: clean up duplicate code with command_object - auto d = DoorsRepository::NewEntity(); - - d.zone = shortname; - d.id = 1000000000 + Strings::ToInt(row[0]); // Out of range of normal use for doors.id - d.doorid = -1; // Client doesn't care if these are all the same door_id - d.pos_x = Strings::ToFloat(row[2]); // xpos - d.pos_y = Strings::ToFloat(row[3]); // ypos - d.pos_z = Strings::ToFloat(row[4]); // zpos - d.heading = Strings::ToFloat(row[5]); // heading - - d.name = row[8]; // objectname - - // Strip trailing "_ACTORDEF" if present. Client won't accept it for doors. - int pos = d.name.size() - strlen("_ACTORDEF"); - if (pos > 0 && d.name.compare(pos, std::string::npos, "_ACTORDEF") == 0) - { - d.name.erase(pos); } - d.dest_zone = "NONE"; + auto d = DoorsRepository::NewEntity(); - if ((d.size = Strings::ToInt(row[11])) == 0) // unknown08 = optional size percentage + d.zone = zone_short_name; + d.id = 1000000000 + e.id; + d.doorid = -1; + d.pos_x = e.xpos; + d.pos_y = e.ypos; + d.pos_z = e.zpos; + d.heading = e.heading; + d.name = Strings::Replace(e.objectname, "_ACTORDEF", ""); + d.dest_zone = "NONE"; + d.incline = e.incline; + d.client_version_mask = 0xFFFFFFFF; + + if (e.size_percentage == 0) { d.size = 100; + } - switch (d.opentype = Strings::ToInt(row[12])) // unknown10 = optional request_nonsolid (0 or 1 or experimental number) + switch (d.opentype = e.solid_type) { - case 0: - d.opentype = 31; - break; - case 1: - d.opentype = 9; - break; + case 0: + d.opentype = 31; + break; + case 1: + d.opentype = 9; + break; } - d.incline = Strings::ToInt(row[13]); // unknown20 = optional model incline value - d.client_version_mask = 0xFFFFFFFF; // We should load the mask from the zone. - auto door = new Doors(d); entity_list.AddDoor(door); } - Object_Struct data = {0}; - uint32 id = 0; - uint32 icon = 0; - uint32 type = 0; - uint32 itemid = 0; - uint32 idx = 0; - int16 charges = 0; - - id = (uint32)Strings::ToInt(row[0]); - data.zone_id = Strings::ToInt(row[1]); - data.x = Strings::ToFloat(row[2]); - data.y = Strings::ToFloat(row[3]); - data.z = Strings::ToFloat(row[4]); - data.heading = Strings::ToFloat(row[5]); - itemid = (uint32)Strings::ToInt(row[6]); - charges = (int16)Strings::ToInt(row[7]); - strcpy(data.object_name, row[8]); - type = (uint8)Strings::ToInt(row[9]); - icon = (uint32)Strings::ToInt(row[10]); + Object_Struct data = {0}; + uint32 id = 0; + uint32 icon = 0; + uint32 type = 0; + uint32 itemid = 0; + uint32 idx = 0; + int16 charges = 0; + + id = e.id; + + data.zone_id = e.zoneid; + data.x = e.xpos; + data.y = e.ypos; + data.z = e.zpos; + data.heading = e.heading; + + itemid = e.itemid; + charges = e.charges; + type = e.type; + icon = e.icon; + data.object_type = type; data.linked_list_addr[0] = 0; data.linked_list_addr[1] = 0; - data.solidtype = (uint32)Strings::ToInt(row[12]); - data.unknown020 = (uint32)Strings::ToInt(row[13]); - data.unknown024 = (uint32)Strings::ToInt(row[14]); - data.unknown076 = (uint32)Strings::ToInt(row[15]); - data.size = Strings::ToFloat(row[16]); - data.tilt_x = Strings::ToFloat(row[17]); - data.tilt_y = Strings::ToFloat(row[18]); + strn0cpy(data.object_name, e.objectname.c_str(), sizeof(data.object_name)); + + data.solid_type = e.solid_type; + data.incline = e.incline; + data.unknown024 = e.unknown24; + data.unknown076 = e.unknown76; + data.size = e.size; + data.tilt_x = e.tilt_x; + data.tilt_y = e.tilt_y; data.unknown084 = 0; @@ -280,9 +274,8 @@ bool Zone::LoadZoneObjects() } EQ::ItemInstance *inst = nullptr; - // FatherNitwit: this dosent seem to work... - // tradeskill containers do not have an itemid of 0... at least what I am seeing - if (itemid == 0) { + // tradeskill containers do not have an itemid of 0 + if (!itemid) { // Generic tradeskill container inst = new EQ::ItemInstance(ItemInstWorldContainer); } else { @@ -290,8 +283,7 @@ bool Zone::LoadZoneObjects() inst = database.CreateItem(itemid); } - // Father Nitwit's fix... not perfect... - if (inst == nullptr && type != OT_DROPPEDITEM) { + if (!inst && type != ObjectTypes::Temporary) { inst = new EQ::ItemInstance(ItemInstWorldContainer); } @@ -301,15 +293,17 @@ bool Zone::LoadZoneObjects() } auto object = new Object(id, type, icon, data, inst); - object->SetDisplayName(row[19]); + object->SetDisplayName(e.display_name.c_str()); entity_list.AddObject(object, false); - if (type == OT_DROPPEDITEM && itemid != 0) + + if (type == ObjectTypes::Temporary && itemid) { entity_list.RemoveObject(object->GetID()); + } safe_delete(inst); } - LogInfo("Loaded [{}] world objects", Strings::Commify(results.RowCount())); + LogInfo("Loaded [{}] world objects", Strings::Commify(l.size())); return true; }