mirror of
https://github.com/EQEmu/Server.git
synced 2025-12-11 21:01:29 +00:00
* Delete EQApplicationPacket after use * Correct issue where Creating EQApplicationPackets in zone/mob.cpp may not free pBuffer[] * Handle freeing pBuffer[] if exists in zone/mob.cpp Create*Packet functions * Handle freeing pBuffer[] in zone/object.cpp Create*Packet methods * Delete leaked outapp variables in zone/trading.cpp * Make CreateHorseSpawnPacket() safer by freeing pBuffer[] before replacing with new[] * Prevent initial new ServerPacket from being leaked in command_reload * Free pack after sending in command_setlsinfo * Delete new NPC in command_viewnpctype after printing stats * Fix compile error * Delete EQApplicationPacket after sending in command_kick * Remove unneeded safe_delete(outapp) after FastQueuePacket and fix minor whitespace issue * Delete packet after sending in WorldServer::SendReloadTasks * Cleanup logic and free leaked NPCType in Client::Doppelganger * Free RespawnOption in Client::HandleRespawnFromHover in 'unexpected rez from hover request' branch * Free EQApplicationPacket after sending in Bot::ProcessBotInspectionRequest * Free Bot when returning from failed Bot::Save() in helper_bot_create() * Initialize variable to nullptr to prevent garbage initial value * Undo change in other PR
1264 lines
38 KiB
C++
Executable File
1264 lines
38 KiB
C++
Executable File
#include "../client.h"
|
|
#include "../object.h"
|
|
#include "../doors.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 = atoi(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 = atoi(row[0]);
|
|
od.x = atof(row[1]);
|
|
od.y = atof(row[2]);
|
|
od.z = atof(row[3]);
|
|
od.heading = atof(row[4]);
|
|
itemid = atoi(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 = atoi(row[7]);
|
|
icon = atoi(row[8]);
|
|
od.size = atoi(row[9]);
|
|
od.solidtype = atoi(row[10]);
|
|
od.unknown020 = atoi(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 = atoi(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 = atoi(sep->arg[2 + col]);
|
|
|
|
switch (od.object_type) {
|
|
case 0: // Static Object
|
|
if ((sep->argnum - col) > 3) {
|
|
od.size = atoi(sep->arg[4 + col]); // Size specified
|
|
|
|
if ((sep->argnum - col) > 4) {
|
|
od.solidtype = atoi(sep->arg[5 + col]); // SolidType specified
|
|
|
|
if ((sep->argnum - col) > 5) {
|
|
od.unknown020 = atoi(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) ? atoi(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 (atoi(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", atoi(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 = atoi(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 = atoi(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 = atoi(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 = atoi(row[0]);
|
|
od.zone_instance = atoi(row[1]);
|
|
od.object_type = atoi(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 = atoi(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 = atoi(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 = atoi(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 = atoi(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 = atoi(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 = atoi(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 = atoi(row[0]);
|
|
od.zone_instance = atoi(row[1]);
|
|
od.object_type = atoi(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 = atof(sep->arg[3]);
|
|
if (sep->argnum > 3) {
|
|
od.y = atof(sep->arg[4]);
|
|
}
|
|
else {
|
|
o->GetLocation(nullptr, &od.y, nullptr);
|
|
}
|
|
|
|
if (sep->argnum > 4) {
|
|
od.z = atof(sep->arg[5]);
|
|
}
|
|
else {
|
|
o->GetLocation(nullptr, nullptr, &od.z);
|
|
}
|
|
|
|
if (sep->argnum > 5) {
|
|
o->SetHeading(atof(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 = atoi(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(atof(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 = atoi(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 = atoi(row[0]);
|
|
od.zone_instance = atoi(row[1]);
|
|
od.object_type = atoi(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 = atoi(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 = atoi(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 (atoi(row[0]) != zone->GetZoneID()) {
|
|
c->Message(Chat::White, "ERROR: Object %u is not part of this zone.", id);
|
|
return;
|
|
}
|
|
|
|
// Wrong Instance Version?
|
|
if (atoi(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 = atoi(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 (atoi(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 = atoi(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 = atof(row[0]);
|
|
od.y = atof(row[1]);
|
|
od.z = atof(row[2]);
|
|
od.heading = atof(row[3]);
|
|
strn0cpy(od.object_name, row[4], sizeof(od.object_name));
|
|
od.object_type = atoi(row[5]);
|
|
icon = atoi(row[6]);
|
|
od.size = atoi(row[7]);
|
|
od.solidtype = atoi(row[8]);
|
|
od.unknown020 = atoi(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);
|
|
}
|
|
|