PDA

View Full Version : New #Object Command


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

trevius
07-16-2009, 02:57 AM
/emote cheer

Bravo, Shendare, bravo! That is just awesome!

I will get this tested out right now and up on the SVN ASAP unless someone is already working on getting it put in. This is a HUGE help to anyone wanting to mess with objects and the related doors stuff will help just as much. Even before similar door commands are in, I am sure this could help to make adding doors much easier too. Great work! And yeah, that command is a doozy!

trevius
07-16-2009, 03:46 AM
Awwe, compile error:

entity.cpp: In member function ‘Object* EntityList::FindNearbyObject(float, float, float, float)’:
entity.cpp:774: error: call of overloaded ‘abs(float)’ is ambiguous
/usr/include/stdlib.h:778: note: candidates are: int abs(int)
/usr/lib/gcc/i486-linux-gnu/4.1.2/../../../../include/c++/4.1.2/cstdlib:172: note: long long int __gnu_cxx::abs(long long int)
/usr/lib/gcc/i486-linux-gnu/4.1.2/../../../../include/c++/4.1.2/cstdlib:142: note: long int std::abs(long int)
entity.cpp:774: error: call of overloaded ‘abs(float)’ is ambiguous
/usr/include/stdlib.h:778: note: candidates are: int abs(int)
/usr/lib/gcc/i486-linux-gnu/4.1.2/../../../../include/c++/4.1.2/cstdlib:172: note: long long int __gnu_cxx::abs(long long int)
/usr/lib/gcc/i486-linux-gnu/4.1.2/../../../../include/c++/4.1.2/cstdlib:142: note: long int std::abs(long int)
entity.cpp:774: error: call of overloaded ‘abs(float)’ is ambiguous
/usr/include/stdlib.h:778: note: candidates are: int abs(int)
/usr/lib/gcc/i486-linux-gnu/4.1.2/../../../../include/c++/4.1.2/cstdlib:172: note: long long int __gnu_cxx::abs(long long int)
/usr/lib/gcc/i486-linux-gnu/4.1.2/../../../../include/c++/4.1.2/cstdlib:142: note: long int std::abs(long int)

Maybe I missed something or messed something up while adjusting the formatting. All I really did was change the double spaces to tabs to make it uniform with the source code. I am going to check it over again and see if I can figure it out. Seems like it should work, since the doors code right above it is almost the exact same thing. BTW, that command in command.cpp is massive!

Here is the bit of code it is referring to:
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();
}

Maybe it is just an issue with my libraries or gcc or kernel or something. I don't really know.

trevius
07-16-2009, 04:11 AM
Commented that while out for now to try to get around it and now getting these compile errors:

command.cpp:13397: error: ‘_strlwr’ was not declared in this scope
command.cpp:13447: error: ‘sprintf_s’ was not declared in this scope
command.cpp:13458: error: ‘sprintf_s’ was not declared in this scope
command.cpp:13472: error: ‘_TRUNCATE’ was not declared in this scope
command.cpp:13472: error: ‘strncpy_s’ was not declared in this scope
command.cpp:13595: error: ‘sprintf_s’ was not declared in this scope
command.cpp:13600: warning: suggest parentheses around assignment used as truth value
command.cpp:13644: error: ‘sprintf_s’ was not declared in this scope
command.cpp:13649: warning: suggest parentheses around assignment used as truth value
command.cpp:13674: error: ‘_TRUNCATE’ was not declared in this scope
command.cpp:13674: error: ‘strncpy_s’ was not declared in this scope
command.cpp:13676: warning: comparison between signed and unsigned integer expressions
command.cpp:13688: error: ‘_strupr’ was not declared in this scope
command.cpp:13703: error: ‘strcpy_s’ was not declared in this scope

Kobaz
07-16-2009, 06:03 AM
I've hit this in my "trying to learn C++ adventure". The *_s functions are Microsoft only. For sprintf_s you might try the POSIX snprintf:

int snprintf(char *str, size_t size, const char *format, ...);

Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

snprintf(), vsnprintf(): _BSD_SOURCE || _XOPEN_SOURCE >= 500 ||
_ISOC99_SOURCE; or cc -std=c99

The functions snprintf() and vsnprintf() write at most size bytes
(including the trailing null byte ('\0')) to str.

This is from the man pages with glibc version 2.2

Shendare
07-16-2009, 09:47 AM
Gah, portability issues from Microsoft, how utterly shocking.

No biggie, let me find the more portable versions of all of the functions that appear to be MS-specific and I'll post a quick diff here.

Shendare
07-16-2009, 11:26 AM
New version of entity.cpp (Line 579) - EntityList::FindNearbyObject()

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);

ox = (x < ox) ? (ox - x) : (x - ox);
oy = (y < oy) ? (oy - y) : (y - oy);
oz = (z < oz) ? (oz - z) : (z - oz);

if ((ox <= radius) && (oy <= radius) && (oz <= radius))
{
return object;
}
iterator.Advance();
}

return NULL;
}


Out of several options available for fixing the gcc ambiguity problem, I simply removed the abs() calls altogether in favor of conditional assignments. Probably more efficient anyway.

New version of command.cpp (Line 13344) - command_object()

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)
{
len = snprintf(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
{
len = snprintf(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, len, 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(od.object_name, row[col++], 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[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.

len = snprintf(query, sizeof(query), "SELECT COUNT(*) FROM object WHERE ID=%u", id);

// Already in database?
if (database.RunQuery(query, len, errbuf, &result))
{
if ((row = mysql_fetch_row(result)) != NULL)
{
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?)
len = snprintf(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, len, errbuf, &result))
{
if ((row = mysql_fetch_row(result)) != NULL)
{
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(od.object_name, sep->arg[3 + col], sizeof(od.object_name));
od.object_name[sizeof(od.object_name) - 1] = '\0'; // Required if strlen(arg) exactly == sizeof(object_name)

len = strlen(od.object_name);
for (col = 0; col < (int32)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.

strncpy(query, "SELECT MAX(id) FROM object", sizeof(query));

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.

len = snprintf(query, sizeof(query), "SELECT zoneid, version, type FROM object WHERE id=%u", id);

iObjectsFound = 0;
if (database.RunQuery(query, len, 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

len = snprintf(query, sizeof(query), "UPDATE object SET type=%u WHERE id=%u", TempStaticType, id);

database.RunQuery(query, len);

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(od.object_name, sep->arg[4], sizeof(od.object_name));
od.object_name[sizeof(od.object_name) - 1] = '\0'; // Required if strlen(arg) exactly == sizeof(object_name)

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)))
{
len = snprintf(query, sizeof(query), "SELECT zoneid, version, type FROM object WHERE id=%u", id);

if ((!database.RunQuery(query, len, 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)
{
len = snprintf(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
{
len = snprintf(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
{
len = snprintf(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, len, 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(door.zone_name, zone->GetShortName(), sizeof(door.zone_name));
door.zone_name[sizeof(door.zone_name) - 1] = '\0'; // Required if strlen(shortname) exactly == sizeof(door.zone_name)

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(door.door_name, od.object_name, sizeof(door.door_name)); // objectname
door.door_name[sizeof(door.door_name) - 1] = '\0'; // Required if strlen(object_name) exactly == sizeof(door.door_name)

// 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

len = snprintf(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, len, 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]);

len = snprintf(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, len, 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.

len = snprintf(query, sizeof(query), "SELECT zoneid, version FROM object WHERE id=%u", id);

if (database.RunQuery(query, len, 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);

len = snprintf(query, sizeof(query),
"SELECT xpos, ypos, zpos, heading, objectname, type, icon, unknown08, unknown10, unknown20"
" FROM object WHERE id=%u", id);

if ((!database.RunQuery(query, len, 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(od.object_name, row[col++], 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[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;
}
}


In addition to changing the above-noted MS-specific function calls to the POSIX portable versions, I took advantage of the snprintf() function returning the assembled string's length to eliminate several unnecessary strlen() calls.

Heh, VC++ gave me "deprecated function" warnings after switching to the POSIX function usage. *shakefist @ MS*

Let me know if there's anything else gcc doesn't like.

Secrets
07-16-2009, 12:57 PM
I'll give this a try today. Nice work!

Shendare
07-16-2009, 01:23 PM
On a read-through pass, I noticed a cosmetic glitch.

Line 13628 in command.cpp sends an error message that tries to refer to 'id' after it has been reset to 0.

The fix:


...
if (id == 0)
{
- c->Message(0, "ERROR: An object already exists with the id %u", id);
+ c->Message(0, "ERROR: An object already exists with the id %u", atoi(sep->arg[2]));

return;
}
...

Shendare
07-16-2009, 01:35 PM
Minor typo on Line 13980:


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]);

Shendare
07-17-2009, 12:24 AM
Once the #object command has been tested out and committed to the SVN, I'll get to work porting the functionality to #door and #groundspawn (I"ll probably give that one a '#gs' alias. Heh.)

KLS
07-17-2009, 02:45 AM
MakeAnyLenString() would be preferable to your snprintf in this case, would of saved you a lot of time too, just have to remember to free the pointer it creates afterward.

trevius
07-17-2009, 03:04 AM
Thank you Shendare! I got the changes in and it compiled perfectly this time. I also got it tested and everything I tried worked perfectly. I committed this to SVN R783. It is very cool to finally be able to have options to manipulate objects in game like that. The command will be a huge help to people in need of it, and will make easy work of things that were considerably more time consuming before this was available. So, great job!

From what I have seen so far, the only suggestion I might make would be a way to bool whether or not it is going to move your character when you do a ToMe move. In cases of dealing with large items, the move might be needed, but when trying to fine tune the positioning, it might be nice to just be able to move a slight bit and do another ToMe until it is perfectly placed. Also, having it report it's current loc after being moved that way would be nice, so you could then do an X Y Z move to tweak the position as well. Those are minor things and just things to consider for the future of this command.

I also played with trying to use it to spawn doors, but sadly, I guess door models don't play nice with object packets like I had assumed they would. I guess that anything with a IT<model number> that can be applied to an item as a graphic is considered an object, and anything without that is considered a door and they can't be used interchangeably between the packet types. That puts a bit of a damper on being able to have the option of using object packets to perfectly position doors. I am pretty sure we could still use door packets to do it, but unfortunately I know of no way to remove doors, so it will be a bit harder to do perfectly like with objects without having to restart the zone. If there was any case on live of something considered a door (that uses door packets) that disappears, we could collect that info and make use of it, but I am not aware of a single case of that happening on Live. Maybe ground spawns would work, but I think they work like object packets, so I kinda doubt it.

With the addition of the #object command, I can see other possibilities popping up to make use of spawning and despawning objects in real-time. If a quest command was made that could manipulate them in similar ways, I am sure neat stuff could be done with that. It probably wouldn't get used too often, but is just another neat idea possibility for the future.

Thanks again, Shendare! That is one heck of a chunk of code!

KLS
07-17-2009, 04:23 AM
I still have some reservations on this, don't consider it final.

Shendare
07-17-2009, 10:58 AM
Yeah, KLS, MakeAnyLenString was certainly an option. I went with snprintf instead to lower a bunch of heap allocations I considered unnecessary. I can elaborate.

Utilizing snprintf, the memory for the string gets allocated just once at the beginning of the function, and is reused with each call to snprintf. The only caveat is to make sure I set aside a large enough buffer for all of my possible uses.

With MakeAnyLenString, for anyone reading who isn't familiar with it, the memory for the string is allocated with each MakeAnyLenString call, and re-allocated as many times as necessary within the function to fit the result string, starting with 256 bytes and doubling each time, re-performing the vsnprintf() call and its process time for each re-allocation as well. This means you don't have to worry about how large the string is, but it also means you could end up with three or four heap allocations per call for larger strings, and re-performing the vsnprintf() call for each allocation until you get one that fits within the buffer, potentially wasting time and fragmenting the heap.

If there's a standing dev preference for the use of MakeAnyLenString over snprintf, I have no problem changing the code to use it instead. I just felt I would explain why I didn't in this case, where the query string buffer might be re-used several times in a single function call.

As for your suggestions, Trev, at the very least adding a coordinate report in 'Move ToMe' is a simple and very worthwhile idea, requiring just one line addition at (what shows in my current source as) Line 14145:

File: command.cpp - object_command()

...
// 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);

+ c->Message(0, "Object %u moved to %.1f, %.1f, %.1f, heading %.1f", id, od.x, od.y, od.z, o->GetHeading());
}
else // Move to x, y, z [h]
...


Unfortunately, not bumping the player back when using 'Move ToMe' always caused problems during my debugging. The model would get summoned into my character's location at his feet, and because the model was always considered solid as a moveable tradeskill object, my character then either (1) couldn't move at all and I had to use #goto, or (2) got bumped up atop the object or bumped out of the object. Both rather throw a wrench in fine tuning location by moving your character slightly with another 'Move ToMe'.

However, adding the coordinate report before bumping your character back will allow numeric fine-tuning, at least, and was a very good idea. I should have thought to add it to 'Move' after putting it in 'Add'.

Now, what problem exactly were you having with door models, Trev? I was able to use FELDOOR2 and DOOR1 while debugging the command in Felwithe.

trevius
07-19-2009, 07:16 AM
Interesting... Doors do seem to work just fine when I test your example. The place I was testing them in was Dreadspire and most of the doors there have considerably longer names than doors in older zones. Here are some examples:

OBJ_LIBRY_HIDEDOOR
OBJ_MAINCASTLE_DOOR
OBJ_DINE_LDOOR
OBJ_DINE_RDOOR
OBJ_DNGRM_LDOOR
OBJ_DINERM_RDOOR
OBJ_MAINCASTLE_DOOR
OBJ_HIDDENDOOR
OBJ_BDRM_LDOOR
OBJ_BDRM_RDOOR
OBJ_LIBRY_SWITCH
MAYONGSWITCHA
MAYONGSWITCHB
OBJ_MAINCASTLE_DOOR
OBJ_TORTURE_DOOR

I didn't try all of those, but I did try a few. My best guess is that the problem is with the length of the new door names. I made sure to try some under 16 chars, but that didn't make any difference. Maybe we are setting the limit lower than it should be somewhere. Also, for it to work on all doors, we might need to look into increasing the limit from 16 chars to 20 or so. I went through a while back and set it so doors can use 32 chars, because they were previously set to 16 for no reason. From looking at the packet structure on Titanium, it looks like we could easily bump objects up to 32 as well:

/*44*/ char object_name[20]; // Name of object, usually something like IT63_ACTORDEF
/*64*/ float unknown064; // seems like coords, not always valid, all 0 on most world objects
/*68*/ float unknown068; // seems like coords, not always valid, all 0 on most world objects
/*72*/ float unknown072; // seems like coords, not always valid, all 0 on most world objects


Those last 3 fields there are bogus and could be combined with the 20 from the object name to make 32. For some reason, live sends bogus info in their name fields after the name, but it has no use at all and is somewhat random in many cases. I would guess that they do that to throw people like us off, but I'm not falling for it :P

So, if we change the structure, we would also have to change everything else related to object name to be able to handle 32 characters in length. I think that would fix the issue I was seeing in Dreadspire at least. Probably even some old zones would have issues with this limitation, so it wouldn't hurt to get it fixed at some point.

Yeormom
07-22-2009, 11:54 AM
I've always added my objects to the database directly and reloaded the zone. I'm sure some if you will run into the same problem I did with tradeskill objects. Improperly configured tradeskill objects will crash the client with a null pointer exception everytime a person clicks on them. Might be worth adding some additional logic handling in the future if you haven't already and this becomes common usage.