View Single Post
  #1  
Old 07-16-2009, 01:41 AM
Shendare
Dragon
 
Join Date: Apr 2009
Location: California
Posts: 814
Default

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
Code:
...
  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
Code:
...
  //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
Code:
...
  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
Code:
...
      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
Code:
...
    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
Code:
...
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
Code:
...
    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"
Code:
...
      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
Reply With Quote