PDA

View Full Version : Zone Customization - Scenery Objects


Shendare
07-05-2009, 08:43 PM
I have made great progress in my personal project of trying to add static scenery objects to customize my zones.

Adding a static scenery object to a zone is very easy once you know how.

Step 1: Know the model name for the object you're adding (look for per-zone object model inventories in an upcoming new version of my EQ Model Inventory program)

Step 2: Add a record to the "doors" table in your EQ database with the following field settings:

id = Unique number for this record
doorid = Unique number within the doors list for this zone
zone = Zone nick
version = Instance version number (0 for default)
name = Model name from Step 1
pos_x/y/z = Location to place object
heading = Direction object faces (0 - 511)
opentype = 9 (makes object non-solid) or 31 (makes object solid)
dest_zone = NONE
size = 100

Everything else = 0

Step 3: Leave and re-enter your zone and see your new object(s)!

Note: If you make changes to the database and the zone is still running in the zone server, you'll have to do a "#zoneshutdown ZoneNick" before zoning back in, to clear the cache, or your changes won't take effect. A hotkey works well.

This technically adds the object to the zone as a door, but opentypes 9 and 31 contain no animation or sound effect, so nothing happens when the user clicks on them. They're just like static scenery objects, except you can place them wherever you want!

Opentype 9 actually makes objects -mostly- non-solid. I tried looking for a fully non-solid opentype, but the partial that 9 gives appears to be the best we can get. Each object will have a few surfaces that are solid, but most become walk-through.

Opentype 31 makes objects fully solid unless the object was specifically designed to be walk-through, which I've mostly seen in chairs.

I plan to make plenty of use of this ability in my own customized server. :)

Now, if anyone can figure out a way to repop a zone's door list without requiring a zone-out, #zoneshutdown, and zone-in, that'd be way cool.

Shendare
07-05-2009, 09:44 PM
If you're eager to see it in action real quick, I've generated a .txt file that lists all of the models available in all of the *_obj.s3d and *.eqg files as of SoF.

http://www.jondjackson.net/eq/emu/files/SoF_ModelInventory.zip

NOTE 1: If you want to add objects to a zone for use, simply add the model source filename to ZoneNick_assets.txt. For example, to add 'obj_ctent' from 'live_event_objects.eqg' to the zone 'felwithea', create a file in your EQ directory called 'felwithea_assets.txt' containing the line 'live_event_objects.eqg' (omitting the single quotes wherever they're shown here for emphasis).

NOTE 2: Global object models are those in files listed in Resources\GlobalLoad.txt.

NOTE 3: Object names are always in upper case (e.g., IT66, OBJ_CTENT).

NOTE 4: Do NOT use the _assets.txt file to import an actual zone S3D or EQG file! This will load the actual zone geometry into your zone, not the objects! Stick to *_obj.s3d and any EQG files that are not race model sources or zone files.

Example:

1. Create felwithea_assets.txt with the line "live_event_objects.eqg" in it.

2. Add the following record to your doors table:


INSERT INTO doors VALUES (61001, 101, 'felwithea', 0, 'OBJ_CTENT', -5, -5, 0, 256, 31, 0, 0, 0, 0, 0, 0, 0, 0, 'NONE', 0, 0, 0, 0, 0, 0, 0, 150, 0);


3. Zone into felwithea and take a look at your new tent!

Enjoy. :)

Shendare
07-05-2009, 09:55 PM
Of course, you don't have to import any object models to add a customized object to your zone. Just ignore the ZoneNick_assets.txt step and use the model name for an object already inside your zone, such as 'FELBED' in felwithea in the example above.

Shendare
07-06-2009, 12:51 AM
Technically, the best thing to do to utilize this availability without cluttering up the doors table with non-door objects would be to create a new table:


CREATE TABLE `static_objects` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`zoneid` INTEGER UNSIGNED NOT NULL DEFAULT 0,
`version` INTEGER UNSIGNED NOT NULL DEFAULT 0,
`x` FLOAT NOT NULL DEFAULT 0,
`y` FLOAT NOT NULL DEFAULT 0,
`z` FLOAT NOT NULL DEFAULT 0,
`heading` FLOAT NOT NULL DEFAULT 0,
`size` SMALLINT UNSIGNED NOT NULL DEFAULT 100,
`incline` INTEGER NOT NULL DEFAULT 0,
`model` VARCHAR(32) NOT NULL,
`solid` TINYINT UNSIGNED NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
INDEX `idx_staticobjects_zoneid`(`zoneid`),
INDEX `idx_staticobjects_version`(`version`)
)
ENGINE = InnoDB;


Then piggy-back static object loading in the door spawns of the entity_list.

File: zone.h, Line 111 - Class Zone

...
void LoadZoneDoors(const char* zone, int16 version);
+ void LoadZoneStaticObjects(int32 zoneid, const char* shortname, int16 version);
bool LoadZoneObjects();
...


File: zone.cpp, Line 698 - new function Zone::LoadZoneStaticObjects()

void Zone::LoadZoneStaticObjects(int32 zoneid, const char* shortname, int16 version)
{
if ((!zoneid) || (!shortname))
{
return;
}

LogFile->write(EQEMuLog::Status, "Loading static objects for %s ...", shortname);

int32 maxdoorid = 1; // If there aren't any doors for this zone, start with #1
int32 maxid = 2000000000; // Way out of range of normal use

char errbuf[MYSQL_ERRMSG_SIZE];
char query[256];

MYSQL_RES *result;
MYSQL_ROW row;

sprintf(query, "SELECT MAX(doorid) FROM doors WHERE zone='%s' AND version=%u", shortname, version);
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
maxdoorid = atoi(row[0]) + 1;
}

mysql_free_result(result);
}

sprintf(query, "SELECT MAX(id) FROM doors");
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
if (row = mysql_fetch_row(result))
{
maxid = atoi(row[0]) + 1;
}

mysql_free_result(result);
}

sprintf(query, "SELECT x, y, z, heading, size, incline, model, solid FROM static_objects WHERE zoneid=%d AND version=%d", zoneid, version);
if (database.RunQuery(query, strlen(query), errbuf, &result))
{
Door newdoor;
Doors* door;
memset(&newdoor, 0, sizeof(Door));
int32 r = 0;
int32 c;

strncpy(newdoor.zone_name, shortname, 16);
memcpy(newdoor.dest_zone, "NONE", 5);

for (r = 0; row = mysql_fetch_row(result); r++)
{
c = 0;
newdoor.db_id = maxid + r;
newdoor.door_id = maxdoorid + r;
newdoor.pos_x = atof(row[c++]);
newdoor.pos_y = atof(row[c++]);
newdoor.pos_z = atof(row[c++]);
newdoor.heading = atof(row[c++]);
newdoor.size = atoi(row[c++]);
newdoor.incline = atoi(row[c++]);
strncpy(newdoor.door_name, row[c++], 32);
switch (newdoor.opentype = atoi(row[c++]))
{
case 0:
newdoor.opentype = 9;
break;
case 1:
newdoor.opentype = 31;
break;
}

door = new Doors(&newdoor);
entity_list.AddDoor(door);
}

mysql_free_result(result);
}
}


File: zone.cpp, Line 954 - Zone::Init()

...
zone->LoadZoneDoors(zone->GetShortName(), zone->GetInstanceVersion());
+ zone->LoadZoneStaticObjects(zone->GetZoneID(), zone->GetShortName(), zone->GetInstanceVersion());
zone->LoadBlockedSpells(zone->GetZoneID());
...


And it works!

http://www.jondjackson.net/eq/emu/images/ZoneStaticObjects.png

If you're wondering why I'm sticking it in doors, instead of the zone objects table, it's because I couldn't figure out any way to use the zone objects system that didn't involve an openable tradeskill container or pickable ground spawn item.

Anyway, forget adding records to the doors table. Much better to use this new static_objects table!

So_1337
07-06-2009, 08:38 AM
Another stunning customization miracle you've worked up! Bravo! I can definitely see some potential in using this to help build future zones as well, marvelous.

Yeormom
07-06-2009, 01:07 PM
I played with static objects back in March and had very good luck with them as well. There we're a few instances where models took extra modifications to get working, as they created invisible walls of death.

I definitely don't recommend using the doors table however.

Shendare
07-06-2009, 01:25 PM
Yeah, using a different table is definitely the way to go.

What methods were you using to play with static objects?

Kayen
07-06-2009, 01:45 PM
I have used this method in the past to add objects to zones. Has always worked well, just a pita searching through the files and figuring out what everything is.

GeorgeS
07-06-2009, 02:52 PM
Shendare, this is brilliant!

I am playing with this now, and I'll see what I can do with this.

GeorgeS

trevius
07-07-2009, 05:03 AM
Instead of having a 3rd table for objects in zones, couldn't we just add 1 more field to the current zone objects table that would let us toggle if we want it to be sent as a normal object or as a non-clickable door? Then, just adjust the object code to send those type of objects as a door instead with some default values? The zone objects table is already under-utilized due to this limitation, so it would be nice to have the option for this without having to use the misleading doors table for it. For the cases of static objects like this, you should just need the name, the zone, the loc and then set the toggle field to 0 or 1 (whichever means non-clickable) to finalize it and the rest would get ignored. Instead of adding a new field for it, we could even define a certain setting in one of the existing fields to cause it to make the switch. Something like setting the type to 999 should work fine.

I think the reason why that stuff is currently done in the doors table is because that is how they send it on Live. So, we try to duplicate that as close as possible. For the purpose of the packet collectors, it is easiest to handle the DB the same way that Live sends the information. It wouldn't be bad to have another table for it for adding custom stuff in and making it a little easier, but the existing tables already work for that so it isn't really a requirement. If you really wanted to make adding static objects easier, you could make a command that lets you add them in-game by simply typing something like "#objectadd static|door|tradeskill <Object Name>" at the location you want to add the item. Then, after re-zoning, you just adjust the table as needed to make sure it is placed exactly how you wanted it. I am not really apposed to another table for it, though.

Shendare
07-07-2009, 09:59 AM
Utilizing the existing objects table with an update to support static objects is a very good idea, and I'd love to try out making an in-game command.

I'll get to work on it.

Shendare
07-08-2009, 02:08 AM
Okay, thanks for the sanity check, Trev.

Ignore all of the code posted in the thread up to this point.

After some real trial and error work, I was able to whittle things down to the point that static object loading now works perfectly straight out of the existing object table and with only the single following code addition:

File: zone.cpp, Line 181 - LoadZoneObjects()

...
while ((row = mysql_fetch_row(result))) {

// -- New Code Start

if (row[9][0] == '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 = -1; // Client doesn't care if multiples end up with the same id's
d.door_id = -1; // For either of these
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;
}

// -- New Code End

Object_Struct data = {0};
uint32 id = 0;
...


Notes:

- To classify an object record as a static object, set the type to 0. The client doesn't consider 0 a valid object type, so we'll never use it for a valid tradeskill object anyway.

- itemid, charges, and icon are ignored for static objects

- unknown08 serves as the optional Size field, a percentage from 1 to 32767%. Leaving it at 0 renders at normal size (100%).

- unknown10 serves as the optional Request_NonSolid field. If set to 1, the object is rendered as opentype 9, which for some smaller models (e.g., chest1) makes them mostly walk-through. Mileage will vary on a per-model basis.

- unknown20 serves as the optional Incline field, tilting the rendered model along the y axis. Not likely to be used... ever, really, but it's there if someone wants it.

- all other unknown## fields are ignored

I don't know about any limit on the number of static objects the client is willing to take. It lets us use the same door_id value for all of the static objects, so we're not limited by the fact that door_id is a signed 8-bit field, which was my initial concern with adding the objects as doors.

I tried everything I could to get the static objects to send to the client as actual objects, sending all sorts of different values for various fields in the Object struct being sent to the client, but no matter what, the client would always open the player's inventory when the object was clicked on. It's just the way it's wired for objects.

It doesn't seem to matter much, though, since they seem to work quite nicely as nonfunctional doors.

Anyway, give it a try and see if it looks ready for committing, Trev. Seems to work very well now. Thanks again for steering me back to the objects table. It really works out better this way.

Now to work on the in-game command for it.

- Shendare

trevius
07-08-2009, 03:57 AM
One thing that might be a possibility would be to see if we can get objects like that to spawn in real-time when they are created. I know for sure that any model can be set on an item and dropped on the ground to simulate a static object. It should just be sending an object packet to do that. With that being the case, we should be able to use a command to place a fake item on the ground where ever we want just by having the command send the packet for it. Now, that alone might not sound too useful, but by using a little trick, we might be able to make good use of it. Since any object would be clickable as you said, we could probably set those objects so when you click them, they disappear. This would allow you to move around adjusting the position of an object until you are at the perfect positioning for it. Then, once you have the perfect position, you add something to the end of the command like "perm" to make the next one get added to the database. Any of the ones added without perm would just be temporary and used only for finding the right positioning. By using the perm option, it would still generate a fake object in the zone like it was previously doing, but this time it would create an actual entry in the database. Instead of having it enter the object into the objects table, it could put them into the doors table. There could even be an option to chose which table you want it to be permanently added to, so you could place tradeskill containers this way. If this idea worked like I think it should, this would make adding/placing doors, tradeskill containers and any static object a piece of cake.

Another nice use for a command like this would be that you could examine each object/door type that a zone has available without having to zone each time to do so. The zoning part is the biggest pain when manually populating zones with objects and doors.

I have never tried it, but it may be possible to spawn a door in real-time without having to zone as well. I don't see why it wouldn't work, but I have just never tried it. The main reason I suggest spawning them as an object is because it is easy to be able to remove them by clicking on them. The client thinks that the object was picked up if it is set correctly. I don't know of any way to remove a door in a zone without having to zone first.

That is something to think about while expanding the possibilities with objects and doors, anyway. I will try to look the code over that you posted, Shendare. Hopefully I can get it added tomorrow night if I have time to check it out and stuff.

Shendare
07-08-2009, 11:29 AM
Coding doors and objects as a ground spawn item for moving them around until the user is satisfied with their placement is an intriguing idea.

My first thought had been to be to look into sending the DoorSpawn or ObjectSpawn packets in real-time when using a #door create or #object create command. The command would send the client a message containing the ID of the door/object it created, and the user could use a #door move or #object move command, passing the ID.

However, this hinged on the ability to send a DoorDespawn and ObjectDespawn packet as well, and I haven't looked yet to see if it looks like a DoorDespawn opcode and struct are available, or whether it's simply a toggled switch in the DoorSpawn opcode and struct.

Further investigation is needed, but there's certainly promise.

ChaosSlayerZ
07-08-2009, 11:35 AM
I have actualy been playing with this for a while. I mean original Doors table is full of things which not actualy doors. It has tents, barrels etc.
Of course geting them to sit just right was a little bit a pain =)

Here some of my tents and fires in West Karana

http://herbhealernet.web.siteprotect.net/personal/chaosflux/album/westkarana2.jpg

Shendare
07-08-2009, 12:03 PM
Awesome!

Once the object table based placement goes live, you would be able to migrate your objects out of the doors table with a couple of SQL commands if you wanted.


INSERT INTO object (zoneid, xpos, ypos, zpos, heading, objectname, `type`, unknown08, unknown10, unknown20)
SELECT zoneidnumber, pos_x, pos_y, pos_z, heading, `name`, 0, 1 - ((opentype - 9) / 22), size, incline
FROM doors
INNER JOIN zone on zone.short_name = doors.zone
WHERE doors.opentype IN (9, 31);


Then when you're confident they're all now in objects correctly...


DELETE FROM doors WHERE opentype IN (9, 31);

Shendare
07-11-2009, 05:36 PM
atm I am working on trying to make commands to spawn/create/despawn objects in real-time.
Oh, you're working on that? I was working on that, too. How far along have you gotten? Should I just send you what I've been working on? I was implementing it via something like:


#object List All|(radius)
#object Add TypeNum Model [ObjectID]
#object Edit (ObjectID) (PropertyName) (NewValue)
(Properties: itemid, charges, model, type, icon, unknown08/10/20/24/60/64/68/72/76/84)
#object Move (ObjectID) ToMe|(x y z [h])
#object Rotate (ObjectID) (Heading)
#object Save (ObjectID)
#object Copy All|(ObjectID) (InstanceVersion)
#object Delete (ObjectID)


- Shendare

trevius
07-11-2009, 07:44 PM
Oh sweet lol. I was just making something very basic and wasn't very far along just yet. I figured it could always be expanded on later, but wanted to get something working to start off with. Yours is much more in-depth than what I was doing, so I'll just leave it up to you :)

The only thing I would add to what you have so far would be a way to despawn them. We should be able to use the ClickObject packet to handle that part. That isn't really too important though, and can always be added in later. Unless your delete option already does that. I like the move option you have. It sounds like a good idea and not hard to do.

Shendare
07-11-2009, 11:34 PM
Oh sweet lol. I was just making something very basic and wasn't very far along just yet. I figured it could always be expanded on later, but wanted to get something working to start off with. Yours is much more in-depth than what I was doing, so I'll just leave it up to you :)

The only thing I would add to what you have so far would be a way to despawn them. We should be able to use the ClickObject packet to handle that part. That isn't really too important though, and can always be added in later. Unless your delete option already does that. I like the move option you have. It sounds like a good idea and not hard to do.

Per my testing, Delete does do a realtime despawn of the object, as Add does a realtime spawn. This allows object changes to be reflected in realtime by sending a despawn packet and respawn packet after each change. Using "#object delete" instead of attempting to trap ClickObject packets for placeables will prevent having the server do unnecessary processing with every click from every user on every object in the game.

The only caveat is that Doors do not have a Despawn opcode that I could find. They appear to be super-static by design. Unless we just haven't found the Opcode yet (since it's not likely to be found with packet collection from Live servers), I assume at this point that the client expects them to be sent once upon zone-in and never change. This also means that there will not be a Despawn packet for static objects.

The best way I can figure as to how to handle this issue is to spawn static objects and doors as tradeskill objects at first, allowing the user to make any model, location, and rotation changes they want to it, then change them into full-on static door objects when the "#object save" command is used to commit them to the database.

I could also look into utilizing an #object command to temporarily change an already-committed door or static object into a changeable tradeskill object upon the next zone-in, allowing the user to make any changes in-game and then re-commit with "#object save".

Should be entirely doable.

trevius
07-12-2009, 02:56 AM
Note that I moved these posts from the SoF Development thread to here, since this is a better place to discuss it.

That sounds great, Shendare. Using temporary objects (from object packets) for placement and testing and then doors for actually saving them is exactly what I was thinking. I think that is probably the best option and should work out great and should be fairly seamless. I don't think people realize yet just how useful this command will be. It is good for adding custom stuff to zones, but it is also extremely good for adding in doors and such to newer zones that are currently completely empty. Until the packet collectors are working again, there is no easy way to do that, so this command will help a lot. The only way to do it better would be to manually pull the data from the Live packets and put them into the table. That is what I did for Crescent Reach, but not all zones are quite so accessible to collect from.

ChaosSlayerZ
07-12-2009, 03:18 AM
Shendare, how did you know that the tent you used is a global object (normaly its only found in Dranik)?
Is it because its located in live_event_objects.eqg?
does all other thing in live_event_objects.eqg are global?
Are there any other files?

are there any global doors? I am desperately trying to get a door into Guk Ldon

Shendare
07-12-2009, 12:51 PM
The particular tent I used isn't global. I had to add an entry in felwithea_assets.txt to pull in the models from live_event_objects.eqg.

To know what models are available globally, make a list of the files referenced in your EQClient\Resources\GlobalLoad.txt file.

Then check those files in the Model Inventory I linked to earlier, and any model names in that file's entry will be available globally.

http://www.jondjackson.net/eq/emu/files/SoF_ModelInventory.zip

You can search the above inventory file for non-global objects available in your particular zone by searching for ZoneNick_obj.s3d or ZoneNick.eqg. The available model names will all be listed there. For guka, for example, I see a model called GUKDOOR700 that looks promising, as well as GUKMETGATE700 and GUKMETGATE701.

I have not found any global door objects via a quick skim of the global model files. However, you can easily add objects from other zones using the ZoneNick_assets.txt feature. Note that this is a client change that has to be done on each client that's going to be playing on your server.

Shendare
07-13-2009, 12:50 AM
Good progress in this project. I'm probably 80% done coding up the #object command. Hopefully by Tuesday or Wednesday night it'll be 100% functional and I'll post the code for it here.

I've now mostly got objects listing with List, spawning with Add, moving with Move, rotating with Rotate, saving with Save, and deleting with Delete. Only Edit and Copy really remain, then a bit of debugging, and I'll post the code for Trev to try out before submission to the SVN. This one will include a pretty hefty new code block in command.cpp, but it should be easy to copy/paste.

It will continue to use the objects table as-is for static objects (type 0) as well as ground spawns (type 1) and tradeskill objects (type 2+). No database structure changes required.

There will be #door commands similar to those for #object that I listed earlier, and they will work pretty much the same. Once I finish up #object, it'll probably be mostly a copy/paste to get #door working properly. Of course there will be no database structure changes for the doors table, either.

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

trevius
07-16-2009, 08:11 AM
Note that I moved the code related portion of this thread into the Server Code Submissions section here:

http://www.eqemulator.net/forums/showthread.php?t=28916

Code related discussion should go there and any other comments on it should go here to keep the code thread as clean as possible.

P.S. I can't wait to try this command out once it compiles properly in Linux!

Bellos
08-16-2010, 05:04 PM
hey guys can you only use objects from .eqg files or can you use em from .s3d files also. If so how do you add from an s3d file as they never show up do you add the whole line like CAMPFIRE_DMSPRITEDEF in the doors table

or do you just add CAMPFIRE or do you skip the DM and do CAMPFIRE_SPRITEDEF

trevius
08-17-2010, 12:28 AM
For most objects (maybe all of them), you need to add _ACTORDEF to the end of the name for them to work. So, "CAMPFIRE" becomes "CAMPFIRE_ACTORDEF".

Also, you might want to use s3dspy or the global model viewer to find your models for objects. The .mod files are the ones you want. Another thing is to make sure you always add the name in all capitals or they won't work.

Bellos
08-17-2010, 12:54 AM
so for CAMPFIRE its CAMPFIRE_ACTORDEF not SPRITEDEF right and only .mod files can be used?


And you can use objects from s3d and eqg right not just eqg?

trevius
08-17-2010, 06:42 AM
You can only use the object from the zone that is loaded. It will be either the eqg or the s3d of the zone you are in. If the zone you are in has both file types, it always loads the eqg version and ignores the s3d version. If you want to use the old version, you can rename your eqg version and restart EQ.

And no, if the full name is CAMPFIRE_SPRITEDEF.mod, then you will need to add CAMPFIRE_SPRITEDEF_ACTORDEF, I think. You can test this out in-game fairly easily by using the #object command. Just type the following:

#object add 12345 0 CAMPFIRE_SPRITEDEF_ACTORDEF

Of course that is only going to work if that model name really exists in the zone you are wanting to use it in. It doesn't look right to me, but I don't know where you got that from.

Bellos
08-17-2010, 11:39 AM
I got the sprite def from ecommons_obj.s3d or everfrost_obj.s3d

trevius
08-17-2010, 02:02 PM
I am not sure about those files. The ones I pull object names from is <zonename>.eqg or <zonename>.s3d, not <zonename>_obj.s3d.

Akkadius
08-17-2010, 02:06 PM
Yeah, some objects only pull by putting '_ACTORDEF' in front of them. Although usually when you try to write it or '#save object' to the database, it gives a write error because the string is too long apparently. But that is the only way some objects pull, especially the 101, 100, type objects.

Bellos
08-17-2010, 04:09 PM
I am not sure about those files. The ones I pull object names from is <zonename>.eqg or <zonename>.s3d, not <zonename>_obj.s3d.

ahh ok, ill try that then.

Bellos
08-17-2010, 04:23 PM
Oh wait do I have to use S3D spy, extract the models i want and put it in my zones model file or do i just put a line for the file in the shortname_assets.txt like was stated in the beginning of this thread.

Also it seems that all the "Models" i was trying to use werent .mod files.

Using S3D spy it seems like only the Newer zones contain . mod files not the older ones. Like Northro does but nro doesnt.

Bellos
08-28-2010, 01:27 AM
Figured id post this here, this is what ive learned

I only know for a fact that you can use files that were originally in the zone.

They will only show up correct after you save the object.

Its also easier to use the database with navicat or whatever to add the object than the in game command. The command seems buggy at setting the height ect...

If the name is OBJ_ROCKS_D in s3dspy that is what you need to put it, you dont need to add _ACTORDEF or _SPRITEDEF after it unless its in the name.

jtcoyle
09-23-2010, 03:16 PM
I want to try my hand at creating 3D content for EQEmulator.

Can someome please head me in the right direction. I have searched for programs to open or edit .s3d files but I have not had much luck.

Any help would be much appreciated.

Joe