Shendare
07-16-2009, 01:41 AM
Wheeeeeeeew, this has been an educational endeavour! LOL.
Alright, I've gotten the #object command 110% functional. I've tested every part of its functionality, but it could most definitely stand to undergo additional testing from more users.
Note: Nothing in the commands is case sensitive.
First the breakdown on the new commands:
----------------
1. #object List - Lists all objects in the zone or within Radius units of you
Usage: #object List All|(Radius)
Examples:
- #object List All
- #object List 500
----------------
2. #object Add - Spawns a new object at your present location
Usage:
Static Object: #object Add [ObjectID] 0 Model [SizePercent] [SolidType] [Incline]
Tradeskill Object: #object Add [ObjectID] TypeNum Model Icon
Examples:
- #object Add 0 CHEST1 50
- #object Add 17 IT66_ACTORDEF 1115
- #object Add 161001 0 FELBED 125 1 0
----------------
3. #object Edit - Changes a property of an object within the zone
Usage: #object Edit (ObjectID) (PropertyName) (Value)
- Static Object Properties: model, type, size, solidtype, incline
- Tradeskill Object Properties: model, type, icon
Examples:
- #object Edit 30005 model BARSTOOL1
- #object Edit 141073 icon 1116
----------------
4. #object Move - Moves an object to a new location
Usage: #object Move (ObjectID) ToMe|(x y z [h])
Examples:
- #object Move 34106 ToMe
- #object Move 1519 -25 50 0 256
----------------
5. #object Rotate - Changes an object's heading without changing its location
Usage: #object Rotate (ObjectID) (NewHeading)
Example: #object Rotate 5029 128
----------------
6. #object Save - Saves an object to the database.
Note: New objects spawned and changes made to existing objects are not saved to the database until #object Save is called on them.
Usage: #object Save (ObjectID)
Example: #object Save 51202
----------------
7. #object Copy - Copies object(s) from the current instance version into another
Usage: #object Copy All|(ObjectID) (InstanceVersion)
Examples:
- #object Copy All 1
- #object Copy 76209 5
----------------
8. #object Delete - Despawns an object and permanently removes it from the database
Usage: #object Delete (ObjectID)
Example: #object Delete 219538
----------------
9. #object Undo - Cancels changes made to an object and respawns it from the database
Note: Objects that have been spawned with '#object Add' but have not yet been saved to the database are deleted when '#object Undo' is used on them.
Usage: #object Undo (ObjectID)
Example: #object Undo 32109
----------------
Okay, that's how they work.
Here are all of the code changes required to make the magic happen. Line numbers are based on Rev 781 (7/15/2009).
File: object.h - Line 172
...
void ClearUser() { user = NULL; }
// **** New Code Start ****
int32 GetDBID();
int32 GetType();
void SetType(int32 type);
int32 GetIcon();
void SetIcon(int32 icon);
int32 GetItemID();
void SetItemID(int32 itemid);
void GetObjectData(Object_Struct* Data);
void SetObjectData(Object_Struct* Data);
void GetLocation(float* x, float* y, float* z);
void SetLocation(float x, float y, float z);
void GetHeading(float* heading);
void SetHeading(float heading);
...
File: object.cpp - Line 630
...
//else {
// Delete contained items, if any
// DeleteWorldContainer(id);
//}
safe_delete_array(query);
}
// **** New Code Start ****
int32 Object::GetDBID()
{
return this->m_id;
}
int32 Object::GetType()
{
return this->m_type;
}
void Object::SetType(uint32 type)
{
this->m_type = type;
this->m_data.object_type = type;
}
int32 Object::GetIcon()
{
return this->m_icon;
}
void Object::SetIcon(int32 icon)
{
this->m_icon = icon;
}
int32 Object::GetItemID()
{
if (this->m_inst == 0)
{
return 0;
}
const Item_Struct* item = this->m_inst->GetItem();
if (item == 0)
{
return 0;
}
return item->ID;
}
void Object::SetItemID(int32 itemid)
{
safe_delete(this->m_inst);
if (itemid)
{
this->m_inst = database.CreateItem(itemid);
}
}
void Object::GetObjectData(Object_Struct* Data)
{
if (Data)
{
memcpy(Data, &this->m_data, sizeof(this->m_data));
}
}
void Object::SetObjectData(Object_Struct* Data)
{
if (Data)
{
memcpy(&this->m_data, Data, sizeof(this->m_data));
}
}
void Object::GetLocation(float* x, float* y, float* z)
{
if (x)
{
*x = this->m_data.x;
}
if (y)
{
*y = this->m_data.y;
}
if (z)
{
*z = this->m_data.z;
}
}
void Object::SetLocation(float x, float y, float z)
{
this->m_data.x = x;
this->m_data.y = y;
this->m_data.z = z;
}
void Object::GetHeading(float* heading)
{
if (heading)
{
*heading = this->m_data.heading;
}
}
void Object::SetHeading(float heading)
{
this->m_data.heading = heading;
}
...
File: entity.h - Line 192
...
void SendAATimer(int32 charid,UseAA_Struct* uaa);
Doors* FindDoor(int8 door_id);
// **** New Code Start ****
Object* FindObject(int32 object_id);
Object* FindNearbyObject(float x, float y, float z, float radius);
...
File: entity.cpp - Line 742
...
return door;
}
iterator.Advance();
}
return 0;
}
// **** New Code Start ****
Object* EntityList::FindObject(int32 object_id)
{
LinkedListIterator<Object*> iterator(object_list);
iterator.Reset();
while(iterator.MoreElements())
{
Object* object=iterator.GetData();
if (object->GetDBID() == object_id)
{
return object;
}
iterator.Advance();
}
return NULL;
}
Object* EntityList::FindNearbyObject(float x, float y, float z, float radius)
{
LinkedListIterator<Object*> iterator(object_list);
iterator.Reset();
float ox;
float oy;
float oz;
while(iterator.MoreElements())
{
Object* object=iterator.GetData();
object->GetLocation(&ox, &oy, &oz);
if ((abs(ox - x) <= radius) && (abs(oy - y) <= radius) && (abs(oz - z) <= radius))
{
return object;
}
iterator.Advance();
}
return NULL;
}
...
File: zone.cpp - Line 181
...
safe_delete_array(query);
LogFile->write(EQEMuLog::Status, "Loading Objects from DB...");
while ((row = mysql_fetch_row(result))) {
// **** New Code Start ****
if (atoi(row[9]) == 0)
{
// Type == 0 - Static Object
const char* shortname = database.GetZoneName(atoi(row[1]), false); // zoneid -> zone_shortname
if (shortname)
{
Door d;
memset(&d, 0, sizeof(d));
strncpy(d.zone_name, shortname, sizeof(d.zone_name));
d.db_id = 1000000000 + atoi(row[0]); // Out of range of normal use for doors.id
d.door_id = -1; // Client doesn't care if these are all the same door_id
d.pos_x = atof(row[2]); // xpos
d.pos_y = atof(row[3]); // ypos
d.pos_z = atof(row[4]); // zpos
d.heading = atof(row[5]); // heading
strncpy(d.door_name, row[8], sizeof(d.door_name)); // objectname
// Strip trailing "_ACTORDEF" if present. Client won't accept it for doors.
int len = strlen(d.door_name);
if ((len > 9) && (memcmp(&d.door_name[len - 9], "_ACTORDEF", 10) == 0))
{
d.door_name[len - 9] = '\0';
}
memcpy(d.dest_zone, "NONE", 5);
if ((d.size = atoi(row[11])) == 0) // unknown08 = optional size percentage
{
d.size = 100;
}
switch (d.opentype = atoi(row[12])) // unknown10 = optional request_nonsolid (0 or 1 or experimental number)
{
case 0:
d.opentype = 31;
break;
case 1:
d.opentype = 9;
break;
}
d.incline = atoi(row[13]); // unknown20 = optional model incline value
Doors* door = new Doors(&d);
entity_list.AddDoor(door);
}
continue;
}
...
File: command.h - Line 309
...
void command_setstartzone(Client *c, const Seperator *sep);
void command_netstats(Client *c, const Seperator *sep);
// **** New Code Start ****
void command_object(Client* c, const Seperator *sep);
...
File: command.cpp - Line 448
...
command_add("setstartzone","[zoneid] - Set target's starting zone. Set to zero to allow the player to use /setstartcity",80,command_setstartzone) ||
- command_add("netstats","Gets the network stats for a stream.",200,command_netstats)
+ command_add("netstats","Gets the network stats for a stream.",200,command_netstats) ||
// **** New Code Start ****
command_add("object","List|Add|Edit|Move|Rotate|Copy|Save|Undo|Delete - Manipulate static and tradeskill objects within the zone",100,command_object)
...
File: command.cpp - Line 13344, AKA "The Doozy"
...
c->Message(0, "Recieved:");
c->Message(0, "Total: %u, per second: %u", c->Connection()->GetBytesRecieved(), c->Connection()->GetBytesRecvPerSecond());
}
}
}
// **** New Code Start ****
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.
char* usage_string = "Usage: #object List|Add|Edit|Move|Rotate|Save|Copy|Delete|Undo";
if ((!sep) || (sep->argnum == 0))
{
// Crash Suppressant: Shouldn't be able to get here, either, but fail gracefully if we do.
c->Message(0, usage_string);
return;
}
char errbuf[MYSQL_ERRMSG_SIZE];
char query[512];
char line[256];
int32 col;
int32 lastid;
MYSQL_RES *result;
MYSQL_ROW row;
int iObjectsFound = 0;
int len;
Object* o = NULL;
Object_Struct od;
Door door;
Doors* doors;
Door_Struct* ds;
int32 id = 0;
int32 itemid = 0;
int32 icon = 0;
int32 instance = 0;
int32 newid = 0;
int16 radius;
EQApplicationPacket* app;
bool bNewObject = false;
errbuf[0] = '\0';
float x2;
float y2;
// Temporary object type for static objects to allow manipulation
// NOTE: Zone::LoadZoneObjects() currently loads this as an int8, so max value is 255!
static const int32 TempStaticType = 255;
// Case insensitive commands (List == list == LIST)
_strlwr(sep->arg[1]);
// Protip: We only really care about the first letter. You can abbreviate Delete to just D if desired.
switch (sep->arg[1][0])
{
case 'l': // List Objects
// 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(0, "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(0, "Objects within this zone:");
}
else
{
c->Message(0, "Objects within %u units of your current location:", radius);
}
if (radius)
{
sprintf_s(query, sizeof(query),
"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
{
sprintf_s(query, sizeof(query),
"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());
}
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
while ((row = mysql_fetch_row(result)))
{
col = 0;
id = atoi(row[col++]);
od.x = atof(row[col++]);
od.y = atof(row[col++]);
od.z = atof(row[col++]);
od.heading = atof(row[col++]);
itemid = atoi(row[col++]);
strncpy_s(od.object_name, sizeof(od.object_name), row[col++], _TRUNCATE);
od.object_type = atoi(row[col++]);
icon = atoi(row[col++]);
od.unknown008[0] = atoi(row[col++]);
od.unknown008[1] = atoi(row[col++]);
od.unknown020 = atoi(row[col++]);
switch (od.object_type)
{
case 0: // Static Object
case TempStaticType: // Static Object unlocked for changes
if (od.unknown008[0] == 0) // Unknown08 field is optional Size parameter for static objects
{
od.unknown008[0] = 100; // Static object default Size is 100%
}
c->Message(0,
"- 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.unknown008[0], od.unknown008[1], od.unknown020);
break;
case OT_DROPPEDITEM: // Ground Spawn
c->Message(0,
"- 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(0,
"- 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;
}
iObjectsFound++;
}
mysql_free_result(result);
}
c->Message(0, "%u object%s found", iObjectsFound, (iObjectsFound == 1) ? "" : "s");
break;
case 'a': // Add Object
// 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(0, "Usage: (Static Object): #object Add [ObjectID] 0 Model [SizePercent] [SolidType] [Incline]");
c->Message(0, "Usage: (Tradeskill Object): #object Add [ObjectID] TypeNum Model Icon");
c->Message(0, "- Notes: Model must start with a letter, max length 16. SolidTypes = 0 (Solid), 1 (Sometimes Non-Solid)");
return;
}
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.unknown008[0] = atoi(sep->arg[4 + col]); // Size specified
if ((sep->argnum - col) > 4)
{
od.unknown008[1] = 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(0, "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;
break;
default: // Everything else == Tradeskill Object
icon = ((sep->argnum - col) > 3) ? atoi(sep->arg[4 + col]) : 0;
if (icon == 0)
{
c->Message(0, "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() * 2.0f; // GetHeading() is half of actual. Compensate by doubling.
if (id)
{
// ID specified. Verify that it doesn't already exist.
sprintf_s(query, sizeof(query), "SELECT COUNT(*) FROM object WHERE ID=%u", id);
// Already in database?
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
if (atoi(row[0]) > 0)
{
// Yep, in database already.
id = 0;
}
}
mysql_free_result(result);
}
if (id)
{
// Not in database. Already spawned, just not saved?
if (entity_list.FindObject(id))
{
// Yep, already spawned.
id = 0;
}
}
if (id == 0)
{
c->Message(0, "ERROR: An object already exists with the id %u", id);
return;
}
}
// Verify no other objects already in this spot (accidental double-click of Hotkey?)
sprintf_s(query, sizeof(query),
"SELECT COUNT(*) FROM object "
"WHERE (zoneid=%u) "
"AND (version=%u) "
"AND (posx BETWEEN %.1f AND %.1f) "
"AND (posy BETWEEN %.1f AND %.1f) "
"AND (posz 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
iObjectsFound = 0;
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
iObjectsFound = atoi(row[0]); // Number of nearby objects from database
}
mysql_free_result(result);
}
if (iObjectsFound == 0)
{
// No objects found in database too close. How about spawned but not yet saved?
if (entity_list.FindNearbyObject(od.x, od.y, od.z, 0.2f))
{
iObjectsFound++;
}
}
if (iObjectsFound)
{
c->Message(0, "ERROR: Object already at this location.");
return;
}
// Strip any single quotes from objectname (SQL injection FTL!)
strncpy_s(od.object_name, sizeof(od.object_name), sep->arg[3 + col], _TRUNCATE); // Database currently only holds 16 characters
len = strlen(od.object_name);
for (col = 0; col < len; col++)
{
if (od.object_name[col] == '\'')
{
// 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(0, "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.
strcpy_s(query, sizeof(query), "SELECT MAX(id) FROM object");
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
id = atoi(row[0]);
}
mysql_free_result(result);
}
id++;
}
// Make sure not to overwrite already-spawned objects that haven't been saved yet.
while (o = entity_list.FindObject(id))
{
id++;
}
if (od.object_type == 0) // Static object
{
od.object_type = TempStaticType; // 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, NULL);
// 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
// GetHeading() returns half of the actual heading, for some reason, so we'll double it here for computation
x2 = 10.0f * sin(c->GetHeading() * 2.0f / 256.0f * 3.14159265f);
y2 = 10.0f * cos(c->GetHeading() * 2.0f / 256.0f * 3.14159265f);
c->MovePC(c->GetX() - x2, c->GetY() - y2, c->GetZ(), c->GetHeading() * 2);
c->Message(0, "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);
if (od.object_type == TempStaticType) // Temporary Static Object
{
c->Message(0, "- 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.");
}
break;
case 'e': // Edit
// Insufficient or invalid arguments
if ((sep->argnum < 2) || ((id = atoi(sep->arg[2])) < 1))
{
c->Message(0, "Usage: #object Edit (ObjectID) [PropertyName] [NewValue]");
c->Message(0, "- Static Object (Type 0) Properties: model, type, size, solidtype, incline");
c->Message(0, "- 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(0, "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.
sprintf_s(query, sizeof(query), "SELECT zoneid, version, type FROM object WHERE id=%u", id);
iObjectsFound = 0;
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
od.zone_id = atoi(row[0]);
od.zone_instance = atoi(row[1]);
od.object_type = atoi(row[2]);
iObjectsFound++;
}
mysql_free_result(result);
}
// Object ID not found?
if (iObjectsFound == 0)
{
c->Message(0, "ERROR: Object %u not found", id);
return;
}
// Object not in this zone?
if (od.zone_id != zone->GetZoneID())
{
c->Message(0, "ERROR: Object %u not in this zone.", id);
return;
}
// Object not in this instance?
if (od.zone_instance != zone->GetInstanceVersion())
{
c->Message(0, "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
sprintf_s(query, sizeof(query), "UPDATE object SET type=%u WHERE id=%u", TempStaticType, id);
database.RunQuery(query, strlen(query));
c->Message(0, "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(0, "NOTE: The change you specified has not been applied, since the static object had not been unlocked for editing yet.");
}
return;
break;
case OT_DROPPEDITEM:
c->Message(0, "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;
break;
case TempStaticType:
c->Message(0, "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;
break;
default:
// Unknown error preventing us from seeing the object in the zone.
c->Message(0, "ERROR: Unknown problem attempting to manipulate object %u", id);
return;
break;
}
}
// 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.
switch (sep->arg[3][0])
{
case 'm':
if (strcmp(sep->arg[3], "model") == 0)
{
if ((sep->arg[4][0] < 'A') || (sep->arg[4][0] > 'Z'))
{
c->Message(0, "ERROR: Model names must begin with a letter.");
return;
}
strncpy_s(od.object_name, sizeof(od.object_name), sep->arg[4], _TRUNCATE);
o->SetObjectData(&od);
c->Message(0, "Object %u now being rendered with model '%s'", id, od.object_name);
}
else
{
id = 0; // Setting ID to 0 will signify invalid input
}
break;
case 't':
if (strcmp(sep->arg[3], "type") == 0)
{
if ((sep->arg[4][0] < '0') || (sep->arg[4][0] > '9'))
{
c->Message(0, "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 = TempStaticType;
c->Message(0, "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(0, "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;
break;
default:
c->Message(0, "Object %u changed to Tradeskill Object Type %u", id, od.object_type);
break;
}
o->SetType(od.object_type);
}
else
{
id = 0; // Setting ID to 0 will signify invalid input
}
break;
case 's':
if (strcmp(sep->arg[3], "size") == 0)
{
if (od.object_type != TempStaticType)
{
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(0, "ERROR: Invalid size specified. Please enter a number.");
return;
}
od.unknown008[0] = atoi(sep->arg[4]);
o->SetObjectData(&od);
if (od.unknown008[0] == 0) // 0 == unspecified == 100%
{
od.unknown008[0] = 100;
}
c->Message(0, "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.unknown008[0]);
}
else if (strcmp(sep->arg[3], "solidtype") == 0)
{
if (od.object_type != TempStaticType)
{
c->Message(0, "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(0, "ERROR: Invalid solidtype specified. Please enter a number.");
return;
}
od.unknown008[1] = atoi(sep->arg[4]);
o->SetObjectData(&od);
c->Message(0, "Static Object %u set to SolidType %u. Change will take effect when you comit 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.unknown008[1]);
}
else
{
id = 0; // Setting ID to 0 will signify invalid input
}
break;
case 'i':
if (strcmp(sep->arg[3], "icon") == 0)
{
if ((od.object_type < 2) || (od.object_type == TempStaticType))
{
c->Message(0, "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(0, "ERROR: Invalid Icon specified. Please enter an icon number.");
return;
}
o->SetIcon(icon);
c->Message(0, "Tradeskill Object %u icon set to %u", id, icon);
}
else if (strcmp(sep->arg[3], "incline") == 0)
{
if (od.object_type != TempStaticType)
{
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(0, "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
{
id = 0; // Setting ID to 0 will signify invalid input
}
break;
default:
id = 0; // Setting ID to 0 will signify invalid input
break;
}
if (id == 0)
{
c->Message(0, "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);
break;
case 'm': // Move
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(0, "Usage: #object Move (ObjectID) ToMe|(x y z [h])");
return;
}
if (!(o = entity_list.FindObject(id)))
{
sprintf_s(query, sizeof(query), "SELECT zoneid, version, type FROM object WHERE id=%u", id);
if ((!database.RunQuery(query, strlen(query), errbuf, &result)) || ((row = mysql_fetch_row(result)) == 0))
{
if (result)
{
mysql_free_result(result);
}
c->Message(0, "ERROR: Object %u not found", id);
return;
}
od.zone_id = atoi(row[0]);
od.zone_instance = atoi(row[1]);
od.object_type = atoi(row[2]);
mysql_free_result(result);
if (od.zone_id != zone->GetZoneID())
{
c->Message(0, "ERROR: Object %u is not in this zone", id);
return;
}
if (od.zone_instance != zone->GetInstanceVersion())
{
c->Message(0, "ERROR: Object %u is not in this instance version", id);
return;
}
switch (od.object_type)
{
case 0:
c->Message(0, "ERROR: Object %u is not yet unlocked for editing. Use '#object Edit' then zone out and back in to move it.", id);
return;
break;
case TempStaticType:
c->Message(0, "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;
break;
case 1:
c->Message(0, "ERROR: Object %u is a temporary spawned object and cannot be manipulated with #object. See the 'ground_spawns' table in the database.", id);
return;
break;
default:
c->Message(0, "ERROR: Object %u not located in zone.", id);
return;
break;
}
}
if ((sep->arg[3][0] & 0xDF) == 'T') // Move To Me
{
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() * 2.0f); // Compensate for GetHeading() returning half of actual
// Bump player back to avoid getting stuck inside object
// GetHeading() returns half of the actual heading, for some reason
x2 = 10.0f * sin(c->GetHeading() * 2.0f / 256.0f * 3.14159265f);
y2 = 10.0f * cos(c->GetHeading() * 2.0f / 256.0f * 3.14159265f);
c->MovePC(c->GetX() - x2, c->GetY() - y2, c->GetZ(), c->GetHeading() * 2.0f);
}
else // Move to x, y, z [h]
{
od.x = atof(sep->arg[3]);
if (sep->argnum > 3)
{
od.y = atof(sep->arg[4]);
}
else
{
o->GetLocation(NULL, &od.y, NULL);
}
if (sep->argnum > 4)
{
od.z = atof(sep->arg[5]);
}
else
{
o->GetLocation(NULL, NULL, &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);
break;
case 'r': // Rotate
// Insufficient or invalid arguments
if ((sep->argnum < 3) || ((id = atoi(sep->arg[2])) == 0))
{
c->Message(0, "Usage: #object Rotate (ObjectID) (Heading, 0-512)");
return;
}
if ((o = entity_list.FindObject(id)) == NULL)
{
c->Message(0, "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);
break;
case 's': // Save
// Insufficient or invalid arguments
if ((sep->argnum < 2) || ((id = atoi(sep->arg[2])) == 0))
{
c->Message(0, "Usage: #object Save (ObjectID)");
return;
}
o = entity_list.FindObject(id);
sprintf(query, "SELECT zoneid, version, type FROM object WHERE id=%u", 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;
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
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;
}
mysql_free_result(result);
}
if (!o)
{
// Object not found in zone. Can't save an object we can't see.
if (bNewObject)
{
c->Message(0, "ERROR: Object %u not found", id);
return;
}
if (od.zone_id != zone->GetZoneID())
{
c->Message(0, "ERROR: Wrong Object ID. %u is not part of this zone.", id);
return;
}
if (od.zone_instance != zone->GetInstanceVersion())
{
c->Message(0, "ERROR: Wrong Object ID. %u is not part of this instance version.", id);
return;
}
if (od.object_type == 0)
{
c->Message(0, "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(0, "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(0, "ERROR: Object %u not found.", id);
return;
}
if ((od.zone_id > 0) && (od.zone_id != zone->GetZoneID()))
{
// Oops! Another GM already saved an object with our id from another zone.
// We'll have to get a new one.
id = 0;
}
if ((id > 0) && (od.zone_instance != zone->GetInstanceVersion()))
{
// Oops! Another GM already saved an object with our id from another instance.
// We'll have to get a new one.
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 == TempStaticType)
{
od.object_type = 0;
}
if (bNewObject)
{
if (id == 0)
{
sprintf_s(query, sizeof(query),
"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.unknown008[0], od.unknown008[1], od.unknown020);
}
else
{
sprintf_s(query, sizeof(query),
"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.unknown008[0], od.unknown008[1], od.unknown020);
}
}
else
{
sprintf_s(query, sizeof(query),
"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.unknown008[0], od.unknown008[1], od.unknown020,
id);
}
if (!database.RunQuery(query, strlen(query), errbuf, 0, &col, &newid))
{
col = 0;
}
if (col == 0)
{
if (errbuf[0] == '\0')
{
// No change made, but no error message given
c->Message(0, "Database Error: Could not save change to Object %u", id);
}
else
{
c->Message(0, "Database Error: %s", errbuf);
}
return;
}
else
{
if (bNewObject)
{
if (newid == id)
{
c->Message(0, "Saved new Object %u to database", id);
}
else
{
c->Message(0, "Saved Object. NOTE: Database returned a new ID number for object: %u", newid);
id = newid;
}
}
else
{
c->Message(0, "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());
memset(&door, 0, sizeof(door));
strncpy_s(door.zone_name, sizeof(door.zone_name), zone->GetShortName(), _TRUNCATE);
door.db_id = 1000000000 + id; // Out of range of normal use for doors.id
door.door_id = -1; // Client doesn't care if these are all the same door_id
door.pos_x = od.x; // xpos
door.pos_y = od.y; // ypos
door.pos_z = od.z; // zpos
door.heading = od.heading; // heading
strncpy_s(door.door_name, sizeof(door.door_name), od.object_name, _TRUNCATE); // objectname
// Strip trailing "_ACTORDEF" if present. Client won't accept it for doors.
len = strlen(door.door_name);
if ((len > 9) && (memcmp(&door.door_name[len - 9], "_ACTORDEF", 10) == 0))
{
door.door_name[len - 9] = '\0';
}
memcpy(door.dest_zone, "NONE", 5);
if ((door.size = od.unknown008[0]) == 0) // unknown08 = optional size percentage
{
door.size = 100;
}
switch (door.opentype = od.unknown008[1]) // 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
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.door_name, 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.door_id;
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(0, "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);
}
break;
case 'c': // Copy
// 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(0, "Usage: #object Copy All|(ObjectID) (InstanceVersion)");
c->Message(0, "- 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(0, "ERROR: Source and destination instance versions are the same.");
return;
}
if ((sep->arg[2][0] & 0xDF) == 'A')
{
// Copy All
sprintf_s(query, sizeof(query),
"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());
if (database.RunQuery(query, strlen(query), errbuf, 0, &col))
{
c->Message(0, "Copied %u object%s into instance version %u", col, (col == 1) ? "" : "s", od.zone_instance);
}
else
{
if (errbuf[0] == '\0')
{
c->Message(0, "Database Error: No objects were copied into instance version %u", od.zone_instance);
}
else
{
c->Message(0, "Database Error: %s", errbuf);
}
}
}
else
{
// Copy ObjectID
id = atoi(sep->arg[2]);
sprintf_s(query, sizeof(query),
"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());
if ((database.RunQuery(query, strlen(query), errbuf, 0, &col)) && (col > 0))
{
c->Message(0, "Copied Object %u into instance version %u", id, od.zone_instance);
}
else
{
// Couldn't copy the object.
if (errbuf[0] == '\0')
{
// No database error returned. See if we can figure out why.
sprintf_s(query, sizeof(query), "SELECT zoneid, version FROM object WHERE id=%u", id);
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
// Wrong ZoneID?
if (atoi(row[0]) != zone->GetZoneID())
{
mysql_free_result(result);
c->Message(0, "ERROR: Object %u is not part of this zone.", id);
return;
}
// Wrong Instance Version?
if (atoi(row[1]) != zone->GetInstanceVersion())
{
mysql_free_result(result);
c->Message(0, "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.
mysql_free_result(result);
c->Message(0, "ERROR: Unknown database error copying Object %u to instance version %u", id, od.zone_instance);
return;
}
mysql_free_result(result);
}
// Typo?
c->Message(0, "ERROR: Object %u not found", id);
}
else
{
c->Message(0, "Database Error: %s", errbuf);
}
}
}
break;
case 'd': // Delete
if ((sep->argnum < 2) || ((id = atoi(sep->arg[2])) <= 0))
{
c->Message(0, "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(NULL, app);
entity_list.RemoveObject(o->GetID());
// 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.
sprintf(query, "DELETE FROM object WHERE (id=%u) AND (zoneid=%u) AND (version=%u) LIMIT 1", id, zone->GetZoneID(), zone->GetInstanceVersion());
database.RunQuery(query, strlen(query));
c->Message(0, "Object %u deleted", id);
}
else
{
// Object not found in zone.
sprintf(query, "SELECT type FROM object WHERE (id=%u) AND (zoneid=%u) AND (version=%u) LIMIT 1", id, zone->GetZoneID(), zone->GetInstanceVersion());
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
switch (atoi(row[0]))
{
case 0: // Static Object
mysql_free_result(result);
sprintf(query, "DELETE FROM object WHERE (id=%u) AND (zoneid=%u) AND (version=%u) LIMIT 1", id, zone->GetZoneID(), zone->GetInstanceVersion());
database.RunQuery(query, strlen(query));
c->Message(0, "Object %u deleted. NOTE: This static object will remain for anyone currently in the zone until they next zone out and in.", id);
mysql_free_result(result);
return;
break;
case 1: // Temporary Spawn
c->Message(0, "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);
mysql_free_result(result);
return;
break;
}
}
mysql_free_result(result);
}
c->Message(0, "ERROR: Object %u not found in this zone or instance!", id);
}
break;
case 'u': // Undo - Reload object from database to undo changes
// Insufficient or invalid arguments
if ((sep->argnum < 2) || ((id = atoi(sep->arg[2])) == 0))
{
c->Message(0, "Usage: #object Undo (ObjectID) -- Reload object from database, undoing any changes you have made");
return;
}
o = entity_list.FindObject(id);
if (!o)
{
c->Message(0, "ERROR: Object %u not found in zone in a manipulable form. No changes to undo.", id);
return;
}
if (o->GetType() == OT_DROPPEDITEM)
{
c->Message(0, "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);
sprintf_s(query, sizeof(query),
"SELECT xpos, ypos, zpos, heading, objectname, type, icon, unknown08, unknown10, unknown20"
" FROM object WHERE id=%u", id);
if ((!database.RunQuery(query, strlen(query), errbuf, &result)) || ((row = mysql_fetch_row(result)) == 0))
{
if (result)
{
mysql_free_result(result);
}
if (errbuf[0] == '\0')
{
c->Message(0, "Database Error: Could not retrieve Object %u from object table.", id);
return;
}
c->Message(0, "Database Error: %s", errbuf);
return;
}
memset(&od, 0, sizeof(od));
col = 0;
od.x = atof(row[col++]);
od.y = atof(row[col++]);
od.z = atof(row[col++]);
od.heading = atof(row[col++]);
strncpy_s(od.object_name, sizeof(od.object_name), row[col++], _TRUNCATE);
od.object_type = atoi(row[col++]);
icon = atoi(row[col++]);
od.unknown008[0] = atoi(row[col++]);
od.unknown008[1] = atoi(row[col++]);
od.unknown020 = atoi(row[col++]);
if (od.object_type == 0)
{
od.object_type = TempStaticType;
}
o = new Object(id, od.object_type, icon, od, NULL);
entity_list.AddObject(o, true);
c->Message(0, "Object %u reloaded from database.", id);
break;
default: // Unrecognized command
c->Message(0, usage_string);
break;
}
}
...
SQL Changes Required: None. :)
Best thing now would be for people to bash the heck out of it and find any glitches, memory leaks, unexpected scenarios, etc. that I may have missed (e.g., did I forget a 'mysql_free_result(result)' anywhere and create a memory leak?).
Next things I could work on would be #door and maybe #groundspawn, utilizing similar functionality.
- Shendare
Alright, I've gotten the #object command 110% functional. I've tested every part of its functionality, but it could most definitely stand to undergo additional testing from more users.
Note: Nothing in the commands is case sensitive.
First the breakdown on the new commands:
----------------
1. #object List - Lists all objects in the zone or within Radius units of you
Usage: #object List All|(Radius)
Examples:
- #object List All
- #object List 500
----------------
2. #object Add - Spawns a new object at your present location
Usage:
Static Object: #object Add [ObjectID] 0 Model [SizePercent] [SolidType] [Incline]
Tradeskill Object: #object Add [ObjectID] TypeNum Model Icon
Examples:
- #object Add 0 CHEST1 50
- #object Add 17 IT66_ACTORDEF 1115
- #object Add 161001 0 FELBED 125 1 0
----------------
3. #object Edit - Changes a property of an object within the zone
Usage: #object Edit (ObjectID) (PropertyName) (Value)
- Static Object Properties: model, type, size, solidtype, incline
- Tradeskill Object Properties: model, type, icon
Examples:
- #object Edit 30005 model BARSTOOL1
- #object Edit 141073 icon 1116
----------------
4. #object Move - Moves an object to a new location
Usage: #object Move (ObjectID) ToMe|(x y z [h])
Examples:
- #object Move 34106 ToMe
- #object Move 1519 -25 50 0 256
----------------
5. #object Rotate - Changes an object's heading without changing its location
Usage: #object Rotate (ObjectID) (NewHeading)
Example: #object Rotate 5029 128
----------------
6. #object Save - Saves an object to the database.
Note: New objects spawned and changes made to existing objects are not saved to the database until #object Save is called on them.
Usage: #object Save (ObjectID)
Example: #object Save 51202
----------------
7. #object Copy - Copies object(s) from the current instance version into another
Usage: #object Copy All|(ObjectID) (InstanceVersion)
Examples:
- #object Copy All 1
- #object Copy 76209 5
----------------
8. #object Delete - Despawns an object and permanently removes it from the database
Usage: #object Delete (ObjectID)
Example: #object Delete 219538
----------------
9. #object Undo - Cancels changes made to an object and respawns it from the database
Note: Objects that have been spawned with '#object Add' but have not yet been saved to the database are deleted when '#object Undo' is used on them.
Usage: #object Undo (ObjectID)
Example: #object Undo 32109
----------------
Okay, that's how they work.
Here are all of the code changes required to make the magic happen. Line numbers are based on Rev 781 (7/15/2009).
File: object.h - Line 172
...
void ClearUser() { user = NULL; }
// **** New Code Start ****
int32 GetDBID();
int32 GetType();
void SetType(int32 type);
int32 GetIcon();
void SetIcon(int32 icon);
int32 GetItemID();
void SetItemID(int32 itemid);
void GetObjectData(Object_Struct* Data);
void SetObjectData(Object_Struct* Data);
void GetLocation(float* x, float* y, float* z);
void SetLocation(float x, float y, float z);
void GetHeading(float* heading);
void SetHeading(float heading);
...
File: object.cpp - Line 630
...
//else {
// Delete contained items, if any
// DeleteWorldContainer(id);
//}
safe_delete_array(query);
}
// **** New Code Start ****
int32 Object::GetDBID()
{
return this->m_id;
}
int32 Object::GetType()
{
return this->m_type;
}
void Object::SetType(uint32 type)
{
this->m_type = type;
this->m_data.object_type = type;
}
int32 Object::GetIcon()
{
return this->m_icon;
}
void Object::SetIcon(int32 icon)
{
this->m_icon = icon;
}
int32 Object::GetItemID()
{
if (this->m_inst == 0)
{
return 0;
}
const Item_Struct* item = this->m_inst->GetItem();
if (item == 0)
{
return 0;
}
return item->ID;
}
void Object::SetItemID(int32 itemid)
{
safe_delete(this->m_inst);
if (itemid)
{
this->m_inst = database.CreateItem(itemid);
}
}
void Object::GetObjectData(Object_Struct* Data)
{
if (Data)
{
memcpy(Data, &this->m_data, sizeof(this->m_data));
}
}
void Object::SetObjectData(Object_Struct* Data)
{
if (Data)
{
memcpy(&this->m_data, Data, sizeof(this->m_data));
}
}
void Object::GetLocation(float* x, float* y, float* z)
{
if (x)
{
*x = this->m_data.x;
}
if (y)
{
*y = this->m_data.y;
}
if (z)
{
*z = this->m_data.z;
}
}
void Object::SetLocation(float x, float y, float z)
{
this->m_data.x = x;
this->m_data.y = y;
this->m_data.z = z;
}
void Object::GetHeading(float* heading)
{
if (heading)
{
*heading = this->m_data.heading;
}
}
void Object::SetHeading(float heading)
{
this->m_data.heading = heading;
}
...
File: entity.h - Line 192
...
void SendAATimer(int32 charid,UseAA_Struct* uaa);
Doors* FindDoor(int8 door_id);
// **** New Code Start ****
Object* FindObject(int32 object_id);
Object* FindNearbyObject(float x, float y, float z, float radius);
...
File: entity.cpp - Line 742
...
return door;
}
iterator.Advance();
}
return 0;
}
// **** New Code Start ****
Object* EntityList::FindObject(int32 object_id)
{
LinkedListIterator<Object*> iterator(object_list);
iterator.Reset();
while(iterator.MoreElements())
{
Object* object=iterator.GetData();
if (object->GetDBID() == object_id)
{
return object;
}
iterator.Advance();
}
return NULL;
}
Object* EntityList::FindNearbyObject(float x, float y, float z, float radius)
{
LinkedListIterator<Object*> iterator(object_list);
iterator.Reset();
float ox;
float oy;
float oz;
while(iterator.MoreElements())
{
Object* object=iterator.GetData();
object->GetLocation(&ox, &oy, &oz);
if ((abs(ox - x) <= radius) && (abs(oy - y) <= radius) && (abs(oz - z) <= radius))
{
return object;
}
iterator.Advance();
}
return NULL;
}
...
File: zone.cpp - Line 181
...
safe_delete_array(query);
LogFile->write(EQEMuLog::Status, "Loading Objects from DB...");
while ((row = mysql_fetch_row(result))) {
// **** New Code Start ****
if (atoi(row[9]) == 0)
{
// Type == 0 - Static Object
const char* shortname = database.GetZoneName(atoi(row[1]), false); // zoneid -> zone_shortname
if (shortname)
{
Door d;
memset(&d, 0, sizeof(d));
strncpy(d.zone_name, shortname, sizeof(d.zone_name));
d.db_id = 1000000000 + atoi(row[0]); // Out of range of normal use for doors.id
d.door_id = -1; // Client doesn't care if these are all the same door_id
d.pos_x = atof(row[2]); // xpos
d.pos_y = atof(row[3]); // ypos
d.pos_z = atof(row[4]); // zpos
d.heading = atof(row[5]); // heading
strncpy(d.door_name, row[8], sizeof(d.door_name)); // objectname
// Strip trailing "_ACTORDEF" if present. Client won't accept it for doors.
int len = strlen(d.door_name);
if ((len > 9) && (memcmp(&d.door_name[len - 9], "_ACTORDEF", 10) == 0))
{
d.door_name[len - 9] = '\0';
}
memcpy(d.dest_zone, "NONE", 5);
if ((d.size = atoi(row[11])) == 0) // unknown08 = optional size percentage
{
d.size = 100;
}
switch (d.opentype = atoi(row[12])) // unknown10 = optional request_nonsolid (0 or 1 or experimental number)
{
case 0:
d.opentype = 31;
break;
case 1:
d.opentype = 9;
break;
}
d.incline = atoi(row[13]); // unknown20 = optional model incline value
Doors* door = new Doors(&d);
entity_list.AddDoor(door);
}
continue;
}
...
File: command.h - Line 309
...
void command_setstartzone(Client *c, const Seperator *sep);
void command_netstats(Client *c, const Seperator *sep);
// **** New Code Start ****
void command_object(Client* c, const Seperator *sep);
...
File: command.cpp - Line 448
...
command_add("setstartzone","[zoneid] - Set target's starting zone. Set to zero to allow the player to use /setstartcity",80,command_setstartzone) ||
- command_add("netstats","Gets the network stats for a stream.",200,command_netstats)
+ command_add("netstats","Gets the network stats for a stream.",200,command_netstats) ||
// **** New Code Start ****
command_add("object","List|Add|Edit|Move|Rotate|Copy|Save|Undo|Delete - Manipulate static and tradeskill objects within the zone",100,command_object)
...
File: command.cpp - Line 13344, AKA "The Doozy"
...
c->Message(0, "Recieved:");
c->Message(0, "Total: %u, per second: %u", c->Connection()->GetBytesRecieved(), c->Connection()->GetBytesRecvPerSecond());
}
}
}
// **** New Code Start ****
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.
char* usage_string = "Usage: #object List|Add|Edit|Move|Rotate|Save|Copy|Delete|Undo";
if ((!sep) || (sep->argnum == 0))
{
// Crash Suppressant: Shouldn't be able to get here, either, but fail gracefully if we do.
c->Message(0, usage_string);
return;
}
char errbuf[MYSQL_ERRMSG_SIZE];
char query[512];
char line[256];
int32 col;
int32 lastid;
MYSQL_RES *result;
MYSQL_ROW row;
int iObjectsFound = 0;
int len;
Object* o = NULL;
Object_Struct od;
Door door;
Doors* doors;
Door_Struct* ds;
int32 id = 0;
int32 itemid = 0;
int32 icon = 0;
int32 instance = 0;
int32 newid = 0;
int16 radius;
EQApplicationPacket* app;
bool bNewObject = false;
errbuf[0] = '\0';
float x2;
float y2;
// Temporary object type for static objects to allow manipulation
// NOTE: Zone::LoadZoneObjects() currently loads this as an int8, so max value is 255!
static const int32 TempStaticType = 255;
// Case insensitive commands (List == list == LIST)
_strlwr(sep->arg[1]);
// Protip: We only really care about the first letter. You can abbreviate Delete to just D if desired.
switch (sep->arg[1][0])
{
case 'l': // List Objects
// 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(0, "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(0, "Objects within this zone:");
}
else
{
c->Message(0, "Objects within %u units of your current location:", radius);
}
if (radius)
{
sprintf_s(query, sizeof(query),
"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
{
sprintf_s(query, sizeof(query),
"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());
}
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
while ((row = mysql_fetch_row(result)))
{
col = 0;
id = atoi(row[col++]);
od.x = atof(row[col++]);
od.y = atof(row[col++]);
od.z = atof(row[col++]);
od.heading = atof(row[col++]);
itemid = atoi(row[col++]);
strncpy_s(od.object_name, sizeof(od.object_name), row[col++], _TRUNCATE);
od.object_type = atoi(row[col++]);
icon = atoi(row[col++]);
od.unknown008[0] = atoi(row[col++]);
od.unknown008[1] = atoi(row[col++]);
od.unknown020 = atoi(row[col++]);
switch (od.object_type)
{
case 0: // Static Object
case TempStaticType: // Static Object unlocked for changes
if (od.unknown008[0] == 0) // Unknown08 field is optional Size parameter for static objects
{
od.unknown008[0] = 100; // Static object default Size is 100%
}
c->Message(0,
"- 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.unknown008[0], od.unknown008[1], od.unknown020);
break;
case OT_DROPPEDITEM: // Ground Spawn
c->Message(0,
"- 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(0,
"- 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;
}
iObjectsFound++;
}
mysql_free_result(result);
}
c->Message(0, "%u object%s found", iObjectsFound, (iObjectsFound == 1) ? "" : "s");
break;
case 'a': // Add Object
// 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(0, "Usage: (Static Object): #object Add [ObjectID] 0 Model [SizePercent] [SolidType] [Incline]");
c->Message(0, "Usage: (Tradeskill Object): #object Add [ObjectID] TypeNum Model Icon");
c->Message(0, "- Notes: Model must start with a letter, max length 16. SolidTypes = 0 (Solid), 1 (Sometimes Non-Solid)");
return;
}
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.unknown008[0] = atoi(sep->arg[4 + col]); // Size specified
if ((sep->argnum - col) > 4)
{
od.unknown008[1] = 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(0, "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;
break;
default: // Everything else == Tradeskill Object
icon = ((sep->argnum - col) > 3) ? atoi(sep->arg[4 + col]) : 0;
if (icon == 0)
{
c->Message(0, "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() * 2.0f; // GetHeading() is half of actual. Compensate by doubling.
if (id)
{
// ID specified. Verify that it doesn't already exist.
sprintf_s(query, sizeof(query), "SELECT COUNT(*) FROM object WHERE ID=%u", id);
// Already in database?
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
if (atoi(row[0]) > 0)
{
// Yep, in database already.
id = 0;
}
}
mysql_free_result(result);
}
if (id)
{
// Not in database. Already spawned, just not saved?
if (entity_list.FindObject(id))
{
// Yep, already spawned.
id = 0;
}
}
if (id == 0)
{
c->Message(0, "ERROR: An object already exists with the id %u", id);
return;
}
}
// Verify no other objects already in this spot (accidental double-click of Hotkey?)
sprintf_s(query, sizeof(query),
"SELECT COUNT(*) FROM object "
"WHERE (zoneid=%u) "
"AND (version=%u) "
"AND (posx BETWEEN %.1f AND %.1f) "
"AND (posy BETWEEN %.1f AND %.1f) "
"AND (posz 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
iObjectsFound = 0;
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
iObjectsFound = atoi(row[0]); // Number of nearby objects from database
}
mysql_free_result(result);
}
if (iObjectsFound == 0)
{
// No objects found in database too close. How about spawned but not yet saved?
if (entity_list.FindNearbyObject(od.x, od.y, od.z, 0.2f))
{
iObjectsFound++;
}
}
if (iObjectsFound)
{
c->Message(0, "ERROR: Object already at this location.");
return;
}
// Strip any single quotes from objectname (SQL injection FTL!)
strncpy_s(od.object_name, sizeof(od.object_name), sep->arg[3 + col], _TRUNCATE); // Database currently only holds 16 characters
len = strlen(od.object_name);
for (col = 0; col < len; col++)
{
if (od.object_name[col] == '\'')
{
// 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(0, "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.
strcpy_s(query, sizeof(query), "SELECT MAX(id) FROM object");
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
id = atoi(row[0]);
}
mysql_free_result(result);
}
id++;
}
// Make sure not to overwrite already-spawned objects that haven't been saved yet.
while (o = entity_list.FindObject(id))
{
id++;
}
if (od.object_type == 0) // Static object
{
od.object_type = TempStaticType; // 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, NULL);
// 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
// GetHeading() returns half of the actual heading, for some reason, so we'll double it here for computation
x2 = 10.0f * sin(c->GetHeading() * 2.0f / 256.0f * 3.14159265f);
y2 = 10.0f * cos(c->GetHeading() * 2.0f / 256.0f * 3.14159265f);
c->MovePC(c->GetX() - x2, c->GetY() - y2, c->GetZ(), c->GetHeading() * 2);
c->Message(0, "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);
if (od.object_type == TempStaticType) // Temporary Static Object
{
c->Message(0, "- 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.");
}
break;
case 'e': // Edit
// Insufficient or invalid arguments
if ((sep->argnum < 2) || ((id = atoi(sep->arg[2])) < 1))
{
c->Message(0, "Usage: #object Edit (ObjectID) [PropertyName] [NewValue]");
c->Message(0, "- Static Object (Type 0) Properties: model, type, size, solidtype, incline");
c->Message(0, "- 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(0, "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.
sprintf_s(query, sizeof(query), "SELECT zoneid, version, type FROM object WHERE id=%u", id);
iObjectsFound = 0;
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
od.zone_id = atoi(row[0]);
od.zone_instance = atoi(row[1]);
od.object_type = atoi(row[2]);
iObjectsFound++;
}
mysql_free_result(result);
}
// Object ID not found?
if (iObjectsFound == 0)
{
c->Message(0, "ERROR: Object %u not found", id);
return;
}
// Object not in this zone?
if (od.zone_id != zone->GetZoneID())
{
c->Message(0, "ERROR: Object %u not in this zone.", id);
return;
}
// Object not in this instance?
if (od.zone_instance != zone->GetInstanceVersion())
{
c->Message(0, "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
sprintf_s(query, sizeof(query), "UPDATE object SET type=%u WHERE id=%u", TempStaticType, id);
database.RunQuery(query, strlen(query));
c->Message(0, "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(0, "NOTE: The change you specified has not been applied, since the static object had not been unlocked for editing yet.");
}
return;
break;
case OT_DROPPEDITEM:
c->Message(0, "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;
break;
case TempStaticType:
c->Message(0, "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;
break;
default:
// Unknown error preventing us from seeing the object in the zone.
c->Message(0, "ERROR: Unknown problem attempting to manipulate object %u", id);
return;
break;
}
}
// 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.
switch (sep->arg[3][0])
{
case 'm':
if (strcmp(sep->arg[3], "model") == 0)
{
if ((sep->arg[4][0] < 'A') || (sep->arg[4][0] > 'Z'))
{
c->Message(0, "ERROR: Model names must begin with a letter.");
return;
}
strncpy_s(od.object_name, sizeof(od.object_name), sep->arg[4], _TRUNCATE);
o->SetObjectData(&od);
c->Message(0, "Object %u now being rendered with model '%s'", id, od.object_name);
}
else
{
id = 0; // Setting ID to 0 will signify invalid input
}
break;
case 't':
if (strcmp(sep->arg[3], "type") == 0)
{
if ((sep->arg[4][0] < '0') || (sep->arg[4][0] > '9'))
{
c->Message(0, "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 = TempStaticType;
c->Message(0, "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(0, "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;
break;
default:
c->Message(0, "Object %u changed to Tradeskill Object Type %u", id, od.object_type);
break;
}
o->SetType(od.object_type);
}
else
{
id = 0; // Setting ID to 0 will signify invalid input
}
break;
case 's':
if (strcmp(sep->arg[3], "size") == 0)
{
if (od.object_type != TempStaticType)
{
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(0, "ERROR: Invalid size specified. Please enter a number.");
return;
}
od.unknown008[0] = atoi(sep->arg[4]);
o->SetObjectData(&od);
if (od.unknown008[0] == 0) // 0 == unspecified == 100%
{
od.unknown008[0] = 100;
}
c->Message(0, "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.unknown008[0]);
}
else if (strcmp(sep->arg[3], "solidtype") == 0)
{
if (od.object_type != TempStaticType)
{
c->Message(0, "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(0, "ERROR: Invalid solidtype specified. Please enter a number.");
return;
}
od.unknown008[1] = atoi(sep->arg[4]);
o->SetObjectData(&od);
c->Message(0, "Static Object %u set to SolidType %u. Change will take effect when you comit 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.unknown008[1]);
}
else
{
id = 0; // Setting ID to 0 will signify invalid input
}
break;
case 'i':
if (strcmp(sep->arg[3], "icon") == 0)
{
if ((od.object_type < 2) || (od.object_type == TempStaticType))
{
c->Message(0, "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(0, "ERROR: Invalid Icon specified. Please enter an icon number.");
return;
}
o->SetIcon(icon);
c->Message(0, "Tradeskill Object %u icon set to %u", id, icon);
}
else if (strcmp(sep->arg[3], "incline") == 0)
{
if (od.object_type != TempStaticType)
{
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(0, "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
{
id = 0; // Setting ID to 0 will signify invalid input
}
break;
default:
id = 0; // Setting ID to 0 will signify invalid input
break;
}
if (id == 0)
{
c->Message(0, "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);
break;
case 'm': // Move
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(0, "Usage: #object Move (ObjectID) ToMe|(x y z [h])");
return;
}
if (!(o = entity_list.FindObject(id)))
{
sprintf_s(query, sizeof(query), "SELECT zoneid, version, type FROM object WHERE id=%u", id);
if ((!database.RunQuery(query, strlen(query), errbuf, &result)) || ((row = mysql_fetch_row(result)) == 0))
{
if (result)
{
mysql_free_result(result);
}
c->Message(0, "ERROR: Object %u not found", id);
return;
}
od.zone_id = atoi(row[0]);
od.zone_instance = atoi(row[1]);
od.object_type = atoi(row[2]);
mysql_free_result(result);
if (od.zone_id != zone->GetZoneID())
{
c->Message(0, "ERROR: Object %u is not in this zone", id);
return;
}
if (od.zone_instance != zone->GetInstanceVersion())
{
c->Message(0, "ERROR: Object %u is not in this instance version", id);
return;
}
switch (od.object_type)
{
case 0:
c->Message(0, "ERROR: Object %u is not yet unlocked for editing. Use '#object Edit' then zone out and back in to move it.", id);
return;
break;
case TempStaticType:
c->Message(0, "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;
break;
case 1:
c->Message(0, "ERROR: Object %u is a temporary spawned object and cannot be manipulated with #object. See the 'ground_spawns' table in the database.", id);
return;
break;
default:
c->Message(0, "ERROR: Object %u not located in zone.", id);
return;
break;
}
}
if ((sep->arg[3][0] & 0xDF) == 'T') // Move To Me
{
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() * 2.0f); // Compensate for GetHeading() returning half of actual
// Bump player back to avoid getting stuck inside object
// GetHeading() returns half of the actual heading, for some reason
x2 = 10.0f * sin(c->GetHeading() * 2.0f / 256.0f * 3.14159265f);
y2 = 10.0f * cos(c->GetHeading() * 2.0f / 256.0f * 3.14159265f);
c->MovePC(c->GetX() - x2, c->GetY() - y2, c->GetZ(), c->GetHeading() * 2.0f);
}
else // Move to x, y, z [h]
{
od.x = atof(sep->arg[3]);
if (sep->argnum > 3)
{
od.y = atof(sep->arg[4]);
}
else
{
o->GetLocation(NULL, &od.y, NULL);
}
if (sep->argnum > 4)
{
od.z = atof(sep->arg[5]);
}
else
{
o->GetLocation(NULL, NULL, &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);
break;
case 'r': // Rotate
// Insufficient or invalid arguments
if ((sep->argnum < 3) || ((id = atoi(sep->arg[2])) == 0))
{
c->Message(0, "Usage: #object Rotate (ObjectID) (Heading, 0-512)");
return;
}
if ((o = entity_list.FindObject(id)) == NULL)
{
c->Message(0, "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);
break;
case 's': // Save
// Insufficient or invalid arguments
if ((sep->argnum < 2) || ((id = atoi(sep->arg[2])) == 0))
{
c->Message(0, "Usage: #object Save (ObjectID)");
return;
}
o = entity_list.FindObject(id);
sprintf(query, "SELECT zoneid, version, type FROM object WHERE id=%u", 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;
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
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;
}
mysql_free_result(result);
}
if (!o)
{
// Object not found in zone. Can't save an object we can't see.
if (bNewObject)
{
c->Message(0, "ERROR: Object %u not found", id);
return;
}
if (od.zone_id != zone->GetZoneID())
{
c->Message(0, "ERROR: Wrong Object ID. %u is not part of this zone.", id);
return;
}
if (od.zone_instance != zone->GetInstanceVersion())
{
c->Message(0, "ERROR: Wrong Object ID. %u is not part of this instance version.", id);
return;
}
if (od.object_type == 0)
{
c->Message(0, "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(0, "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(0, "ERROR: Object %u not found.", id);
return;
}
if ((od.zone_id > 0) && (od.zone_id != zone->GetZoneID()))
{
// Oops! Another GM already saved an object with our id from another zone.
// We'll have to get a new one.
id = 0;
}
if ((id > 0) && (od.zone_instance != zone->GetInstanceVersion()))
{
// Oops! Another GM already saved an object with our id from another instance.
// We'll have to get a new one.
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 == TempStaticType)
{
od.object_type = 0;
}
if (bNewObject)
{
if (id == 0)
{
sprintf_s(query, sizeof(query),
"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.unknown008[0], od.unknown008[1], od.unknown020);
}
else
{
sprintf_s(query, sizeof(query),
"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.unknown008[0], od.unknown008[1], od.unknown020);
}
}
else
{
sprintf_s(query, sizeof(query),
"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.unknown008[0], od.unknown008[1], od.unknown020,
id);
}
if (!database.RunQuery(query, strlen(query), errbuf, 0, &col, &newid))
{
col = 0;
}
if (col == 0)
{
if (errbuf[0] == '\0')
{
// No change made, but no error message given
c->Message(0, "Database Error: Could not save change to Object %u", id);
}
else
{
c->Message(0, "Database Error: %s", errbuf);
}
return;
}
else
{
if (bNewObject)
{
if (newid == id)
{
c->Message(0, "Saved new Object %u to database", id);
}
else
{
c->Message(0, "Saved Object. NOTE: Database returned a new ID number for object: %u", newid);
id = newid;
}
}
else
{
c->Message(0, "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());
memset(&door, 0, sizeof(door));
strncpy_s(door.zone_name, sizeof(door.zone_name), zone->GetShortName(), _TRUNCATE);
door.db_id = 1000000000 + id; // Out of range of normal use for doors.id
door.door_id = -1; // Client doesn't care if these are all the same door_id
door.pos_x = od.x; // xpos
door.pos_y = od.y; // ypos
door.pos_z = od.z; // zpos
door.heading = od.heading; // heading
strncpy_s(door.door_name, sizeof(door.door_name), od.object_name, _TRUNCATE); // objectname
// Strip trailing "_ACTORDEF" if present. Client won't accept it for doors.
len = strlen(door.door_name);
if ((len > 9) && (memcmp(&door.door_name[len - 9], "_ACTORDEF", 10) == 0))
{
door.door_name[len - 9] = '\0';
}
memcpy(door.dest_zone, "NONE", 5);
if ((door.size = od.unknown008[0]) == 0) // unknown08 = optional size percentage
{
door.size = 100;
}
switch (door.opentype = od.unknown008[1]) // 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
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.door_name, 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.door_id;
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(0, "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);
}
break;
case 'c': // Copy
// 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(0, "Usage: #object Copy All|(ObjectID) (InstanceVersion)");
c->Message(0, "- 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(0, "ERROR: Source and destination instance versions are the same.");
return;
}
if ((sep->arg[2][0] & 0xDF) == 'A')
{
// Copy All
sprintf_s(query, sizeof(query),
"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());
if (database.RunQuery(query, strlen(query), errbuf, 0, &col))
{
c->Message(0, "Copied %u object%s into instance version %u", col, (col == 1) ? "" : "s", od.zone_instance);
}
else
{
if (errbuf[0] == '\0')
{
c->Message(0, "Database Error: No objects were copied into instance version %u", od.zone_instance);
}
else
{
c->Message(0, "Database Error: %s", errbuf);
}
}
}
else
{
// Copy ObjectID
id = atoi(sep->arg[2]);
sprintf_s(query, sizeof(query),
"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());
if ((database.RunQuery(query, strlen(query), errbuf, 0, &col)) && (col > 0))
{
c->Message(0, "Copied Object %u into instance version %u", id, od.zone_instance);
}
else
{
// Couldn't copy the object.
if (errbuf[0] == '\0')
{
// No database error returned. See if we can figure out why.
sprintf_s(query, sizeof(query), "SELECT zoneid, version FROM object WHERE id=%u", id);
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
// Wrong ZoneID?
if (atoi(row[0]) != zone->GetZoneID())
{
mysql_free_result(result);
c->Message(0, "ERROR: Object %u is not part of this zone.", id);
return;
}
// Wrong Instance Version?
if (atoi(row[1]) != zone->GetInstanceVersion())
{
mysql_free_result(result);
c->Message(0, "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.
mysql_free_result(result);
c->Message(0, "ERROR: Unknown database error copying Object %u to instance version %u", id, od.zone_instance);
return;
}
mysql_free_result(result);
}
// Typo?
c->Message(0, "ERROR: Object %u not found", id);
}
else
{
c->Message(0, "Database Error: %s", errbuf);
}
}
}
break;
case 'd': // Delete
if ((sep->argnum < 2) || ((id = atoi(sep->arg[2])) <= 0))
{
c->Message(0, "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(NULL, app);
entity_list.RemoveObject(o->GetID());
// 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.
sprintf(query, "DELETE FROM object WHERE (id=%u) AND (zoneid=%u) AND (version=%u) LIMIT 1", id, zone->GetZoneID(), zone->GetInstanceVersion());
database.RunQuery(query, strlen(query));
c->Message(0, "Object %u deleted", id);
}
else
{
// Object not found in zone.
sprintf(query, "SELECT type FROM object WHERE (id=%u) AND (zoneid=%u) AND (version=%u) LIMIT 1", id, zone->GetZoneID(), zone->GetInstanceVersion());
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
switch (atoi(row[0]))
{
case 0: // Static Object
mysql_free_result(result);
sprintf(query, "DELETE FROM object WHERE (id=%u) AND (zoneid=%u) AND (version=%u) LIMIT 1", id, zone->GetZoneID(), zone->GetInstanceVersion());
database.RunQuery(query, strlen(query));
c->Message(0, "Object %u deleted. NOTE: This static object will remain for anyone currently in the zone until they next zone out and in.", id);
mysql_free_result(result);
return;
break;
case 1: // Temporary Spawn
c->Message(0, "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);
mysql_free_result(result);
return;
break;
}
}
mysql_free_result(result);
}
c->Message(0, "ERROR: Object %u not found in this zone or instance!", id);
}
break;
case 'u': // Undo - Reload object from database to undo changes
// Insufficient or invalid arguments
if ((sep->argnum < 2) || ((id = atoi(sep->arg[2])) == 0))
{
c->Message(0, "Usage: #object Undo (ObjectID) -- Reload object from database, undoing any changes you have made");
return;
}
o = entity_list.FindObject(id);
if (!o)
{
c->Message(0, "ERROR: Object %u not found in zone in a manipulable form. No changes to undo.", id);
return;
}
if (o->GetType() == OT_DROPPEDITEM)
{
c->Message(0, "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);
sprintf_s(query, sizeof(query),
"SELECT xpos, ypos, zpos, heading, objectname, type, icon, unknown08, unknown10, unknown20"
" FROM object WHERE id=%u", id);
if ((!database.RunQuery(query, strlen(query), errbuf, &result)) || ((row = mysql_fetch_row(result)) == 0))
{
if (result)
{
mysql_free_result(result);
}
if (errbuf[0] == '\0')
{
c->Message(0, "Database Error: Could not retrieve Object %u from object table.", id);
return;
}
c->Message(0, "Database Error: %s", errbuf);
return;
}
memset(&od, 0, sizeof(od));
col = 0;
od.x = atof(row[col++]);
od.y = atof(row[col++]);
od.z = atof(row[col++]);
od.heading = atof(row[col++]);
strncpy_s(od.object_name, sizeof(od.object_name), row[col++], _TRUNCATE);
od.object_type = atoi(row[col++]);
icon = atoi(row[col++]);
od.unknown008[0] = atoi(row[col++]);
od.unknown008[1] = atoi(row[col++]);
od.unknown020 = atoi(row[col++]);
if (od.object_type == 0)
{
od.object_type = TempStaticType;
}
o = new Object(id, od.object_type, icon, od, NULL);
entity_list.AddObject(o, true);
c->Message(0, "Object %u reloaded from database.", id);
break;
default: // Unrecognized command
c->Message(0, usage_string);
break;
}
}
...
SQL Changes Required: None. :)
Best thing now would be for people to bash the heck out of it and find any glitches, memory leaks, unexpected scenarios, etc. that I may have missed (e.g., did I forget a 'mysql_free_result(result)' anywhere and create a memory leak?).
Next things I could work on would be #door and maybe #groundspawn, utilizing similar functionality.
- Shendare