ArcEmu Core Customization: Server-Player Characters
Background:
Before I begin with the setup, I would like to give credit to iindigo for the conceptual component which serves as the basis of this core customization. On his original thread entitled: An intriguing possibility, there was discussion of utilizing the Mirror Image client-server-message/packet to implement NPCs who appeared exactly as players on the client. However, it appears that no action was actually taken to implement this concept.
The aim of the customization is to implement exactly this functionality such that NPCs will be able to mirror the visuals of legitimate players. This serves as an ArcEmu alternative to the third-party Trinity content which enables "scripted" players. The advantages of this particular approach include:
1) The implementation is light-weight involving only a few minor core edits.
2) It's structured dynamically, taking advantage of MySQL tables.
3) The units used are NPCs and thus can be integrated with ArcEmu's powerful scripting subsystem.
Setup:
To use this customization you'll need:
a) A MySQL database management tool (i.e. Navicat/HeidiSQL)
b) A C++ Integrated Development Environment such as Visual Studio C++ or the equivalent components (i.e. compiler, assembler, linker)
c) The ArcEmu core (any remotely modern revision will suffice; however, I'm using a slightly older revision)
Breakdown: The customization is relatively simple. To the core we will add a single header file which I have named ServerPlayedCharacter.h, a single source file which I have named ServerPlayedCharacter.cpp and we will be slightly modifying the World.cpp, WorldSession.cpp and Unit.cpp source files as well as the StdAfx.h header. In addition, we will be adding a table to the world MySQL database which must be named: server_played_character for which I will provide the SQL structure.
Core Modifications:
We'll first begin with the core modifications. The first step is to create two files (a header file and a source file, respectively). The code for ServerPlayedCharacter.h and ServerPlayedCharacter.cpp is given below.
ServerPlayedCharacter.h Body:
Code:#ifndef __SERVER_PLAYED_CHARACTER_H_ #define __SERVER_PLAYED_CHARACTER_H_ class TaskList; class Unit; enum PlayerRace { P_RACE_HUMAN = 1, P_RACE_ORC = 2, P_RACE_DWARF = 3, P_RACE_NIGHTELF = 4, P_RACE_UNDEAD = 5, P_RACE_TAUREN = 6, P_RACE_GNOME = 7, P_RACE_TROLL = 8, P_RACE_GOBLIN = 9, P_RACE_BLOODELF = 10, P_RACE_DRAENEI = 11, P_RACE_FELORC = 12, P_RACE_NAGA = 13, P_RACE_BROKEN = 14, P_RACE_SKELETON = 15, P_RACE_VRYKUL = 16, P_RACE_TUSKARR = 17, P_RACE_FORESTTROLL = 18, P_RACE_TAUNKA = 19, P_RACE_NORTHRENDSKELETON = 20, P_RACE_ICETROLL = 21, P_NUM_RACES = 21 }; enum PlayerGender { GENDER_MALE = 0, GENDER_FEMALE = 1 }; enum PlayerClass { CLASS_WARRIOR = 1, CLASS_PALADIN = 2, CLASS_HUNTER = 3, CLASS_ROGUE = 4, CLASS_PRIEST = 5, CLASS_DEATHKNIGHT = 6, CLASS_SHAMAN = 7, CLASS_MAGE = 8, CLASS_WARLOCK = 9, CLASS_DRUID = 11 }; enum UnitItemSlot { ITEM_SLOT_HEAD = 0, ITEM_SLOT_SHOULDER = 1, ITEM_SLOT_BODY = 2, ITEM_SLOT_CHEST = 3, ITEM_SLOT_WAIST = 4, ITEM_SLOT_LEGS = 5, ITEM_SLOT_FEET = 6, ITEM_SLOT_WRITST = 7, ITEM_SLOT_HAND = 8, ITEM_SlOT_BACK = 9, ITEM_SLOT_TABARD = 10, ITEM_SLOT_MAINHAND = 11, ITEM_SLOT_OFFHAND = 12, ITEM_SLOT_RANGED = 13, NUM_ITEM_SLOTS = 14 }; struct ServerPlayedCharacter { uint32 entry; uint8 raceId; // also serves as the character's display index uint8 gender; uint8 classId; uint8 skinId; uint8 faceId; uint8 hairStyleId; uint8 hairColorId; uint8 facialHairId; uint32 itemId[14]; }; void LoadServerPlayedNPCFromDB(TaskList&); void UnitLoadMiDisplayInfo(Unit* pUnit); WorldPacket MirrorImagePacketHook(Unit* pUnit); void ServerPlayedNPCCleanup(void); #endif
ServerPlayedCharacter.cpp Body:
Code:#include "StdAfx.h" // Table format const char* gServerPlayedCreatureFormat = "uccccccccuuuuuuuuuuuuuu"; // Table storage SERVER_DECL SQLStorage< ServerPlayedCharacter, HashMapStorageContainer<ServerPlayedCharacter> > ServerPlayedNpcStorage; stdext::hash_set<uint32> mirrorImagePacketHookSet; static uint32 RaceDisplayMap[P_NUM_RACES * 2 + 2] = { 0U, 0U, 49U, 50U, 51U, 52U, 53U, 54U, 55U, 56U, 57U, 58U, 59U, 60U, 1563U, 1564U, 1478U, 1479U, 6894U, 6895U, 15476U, 15475U, 16125U, 16126U, 16981U, 16980U, 17402U, 17403U, 17576U, 17577U, 17578U, 17579U, 21685U, 21686U, 21780U,21780U,21963U, 21964U, 26316U, 26317U, 26871U, 26872U, 26873U, 26874U }; // maps the race index/gender in the DB to the actual display ID void LoadServerPlayedNPCFromDB(TaskList& tl) { tl.AddTask( new Task( new CallbackP2<SQLStorage< ServerPlayedCharacter, HashMapStorageContainer<ServerPlayedCharacter> >, const char *, const char *>(&ServerPlayedNpcStorage, &SQLStorage<ServerPlayedCharacter, HashMapStorageContainer<ServerPlayedCharacter> >::Load, "server_played_character", gServerPlayedCreatureFormat) ) ); } void UnitLoadMiDisplayInfo(Unit* pUnit) { ServerPlayedCharacter* infoSpc = ServerPlayedNpcStorage.LookupEntry(pUnit->GetEntry()); if (!infoSpc) return; if (infoSpc->raceId > (uint8)P_NUM_RACES) { std::cout << "Server-Player NPC with entry ID " << infoSpc->entry << " used an invalid race id: " << infoSpc->raceId << std::endl; infoSpc->raceId = 0; } infoSpc->gender &= 0x1; pUnit->SetDisplayId( RaceDisplayMap[(infoSpc->raceId << 1) | infoSpc->gender] ); pUnit->setRace(infoSpc->raceId); pUnit->setGender(infoSpc->gender); mirrorImagePacketHookSet.insert(pUnit->GetEntry()); pUnit->SetUInt32Value(UNIT_FIELD_FLAGS_2, pUnit->GetUInt32Value(UNIT_FIELD_FLAGS_2) | UNIT_FLAG2_MIRROR_IMAGE); } WorldPacket MirrorImagePacketHook(Unit* pUnit) { ServerPlayedCharacter* infoSpc = ServerPlayedNpcStorage.LookupEntry(pUnit->GetEntry()); Arcemu::Util::ARCEMU_ASSERT(infoSpc != 0); WorldPacket data(SMSG_MIRRORIMAGE_DATA, 68); data << pUnit->GetGUID(); data << RaceDisplayMap[(infoSpc->raceId << 1) | infoSpc->gender]; data << infoSpc->raceId; data << infoSpc->gender; if (infoSpc->classId == 10 || infoSpc->classId > 11) { std::cout << "Server-Played NPC with entry ID " << infoSpc->entry << " used an invalid class id: " << infoSpc->classId << std::endl; infoSpc->classId = 0; } data << infoSpc->classId; data << infoSpc->skinId; data << infoSpc->faceId; data << infoSpc->hairStyleId; data << infoSpc->hairColorId; data << infoSpc->facialHairId; data << static_cast<uint32>(0); for (uint8 i = 0; i < NUM_ITEM_SLOTS - 3; i++) { data << infoSpc->itemId[i]; } pUnit->SetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID, infoSpc->itemId[ITEM_SLOT_MAINHAND]); pUnit->SetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID + 0x1, infoSpc->itemId[ITEM_SLOT_OFFHAND]); pUnit->SetUInt32Value(UNIT_VIRTUAL_ITEM_SLOT_ID + 0x2, infoSpc->itemId[ITEM_SLOT_RANGED]); return data; } void ServerPlayedNPCCleanup(void) { ServerPlayedNpcStorage.Cleanup(); }
You'll need to make sure that both of these files are compiled/linked with the world-server's main code (i.e. in an IDE they are included as part of the main "world" project).
StdAfx.h Edits:
Next, we'll modify the StdAfx.h header file. You'll simply need to add an #include pre-processor directive (the location of which is irrelevant), including the ServerPlayedCharacter.h header so that the identifiers declared within it will be available throughout the world-server code. Within StdAfx.h, simply add the following directive:
Below is the segment of interest of the StdAfx.h header-- the code to insert is highlighted:Code:#include "ServerPlayedCharacter.h"
World.cpp Edits:
Now we're ready to move onto the database loading/releasing in World.cpp. First, look for the function (defined/declared in global scope) called SetInitialWorldSettings(). Within that function, look for the definition of a variable of type TaskList. Below that, there should be a call to the database loader function Storage_FillTaskList() with the TaskList local as its parameter. Right below that call, add the following statement:
It should be noted that the actual argument passed to the function (here, tl) should be the TaskList local variable declared above. Below is the segment of interest of the SetInitialWorldSettings() function-- the code to insert is highlighted:Code:LoadServerPlayedNPCFromDB(tl);
The next function of interest in World.cpp is the World class' destructor method (World::~World()). In this function, we need to call our ServerPlayedNPCCleanup() function to handle closing the table, and releasing the heap-memory used to store its contents. Directly below the call to Storage_Cleanup(), you can insert the following statement:
Below is an image of the segment of interest in the World class' destructor definition-- the code to insert is highlighted:Code:ServerPlayedNPCCleanup();
Unit.cpp Edits:
Now we can move onto the Unit.cpp for some more edits. In Unit.cpp we're looking for the Unit::OnPushToWorld method. As the final statement of this method, we'll call our UnitLoadMiDisplayInfo() function with the invoking Unit object as the parameter. This call should be added as the final statement in the Unit::OnPushToWorld method:
Below is an image of the (entire) updated Unit::OnPushToWorld method-- the code to insert is highlighted:Code:UnitLoadMiDisplayInfo(this);
WorldSession.cpp Edits:
Now we are onto our final core edit: in WorldSession.cpp. First we are going to add a hook mechanism for the client mirror image query opcode handler. At the very top of the file (but below the #include directive for StdAfx.h) add the following external variable declaration:Below is an image of what the new WorldSession.cpp file should look like-- the code to insert is highlighted:Code:extern stdext::hash_set<uint32> mirrorImagePacketHookSet;
Next, we are looking for the WorldSession client message handler for the CMSG_GET_MIRRORIMAGE_DATA query opcode. Conveniently, this happens to be named HandleMirrorImageOpcode, and its qualified name (i.e. the name you're looking for is): WorldSession::HandleMirrorImageOpcode. In this method, there should be code that looks like this:Code:Unit* Image = _player->GetMapMgr()->GetUnit(GUID); if(Image == NULL) return;
Directly below that, add the following code:Code:stdext::hash_set<uint32>::iterator iter = mirrorImagePacketHookSet.find(Image->GetEntry()); if (iter != mirrorImagePacketHookSet.end()) { WorldPacket data = MirrorImagePacketHook(Image); SendPacket(&data); return; }
Below is an image of the segment of interest of the WorldSession::HandleMirrorImageOpcode-- the code to insert is highlighted:
Database Modifications:
Into your world database, you'll need to execute the following SQL, which should insert a table named server_played_creature into the world database:Code:/* Navicat MySQL Data Transfer */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for `server_played_character` -- ---------------------------- DROP TABLE IF EXISTS `server_played_character`; CREATE TABLE `server_played_character` ( `entry` int(10) unsigned NOT NULL, `race_id` tinyint(4) NOT NULL, `gender` tinyint(1) unsigned NOT NULL, `class_index_id` tinyint(4) unsigned NOT NULL, `skin_id` tinyint(4) unsigned NOT NULL, `face_id` tinyint(4) unsigned NOT NULL, `hair_style_id` tinyint(4) unsigned NOT NULL, `hair_color_id` tinyint(4) unsigned NOT NULL, `facial_hair_id` tinyint(4) unsigned NOT NULL, `item_head_id` int(10) unsigned NOT NULL, `item_shoulder_id` int(10) unsigned NOT NULL, `item_body_id` int(10) unsigned NOT NULL, `item_chest_id` int(10) unsigned NOT NULL, `item_waist_id` int(10) unsigned NOT NULL, `item_legs_id` int(10) unsigned NOT NULL, `item_feet_id` int(10) unsigned NOT NULL, `item_wrist_id` int(10) unsigned NOT NULL, `item_hand_id` int(10) unsigned NOT NULL, `item_back_id` int(10) unsigned NOT NULL, `item_tabard_id` int(10) unsigned NOT NULL, `item_mainhand_id` int(10) unsigned NOT NULL, `item_offhand_id` int(10) unsigned NOT NULL, `item_ranged_id` int(10) unsigned NOT NULL, PRIMARY KEY (`entry`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Usage and Other Information:
Usage:
The new world database table, server_played_character has the following structure:
Code:entry (uint32): The entry ID of the npc who will look like a player race_id (uint8): The race_id that the npc will take on; this determines the display ID of the npc (enumerated in ServerPlayedCharacter.h and Player.h gender (uint8): The gender the npc will take on: 0 = male, 1 = female class_index_id (uint8): the class that the npc will take on (enumerated in ServerPlayedCharacter.h and Player.h) skin_id (uint8): the skin identifier (i.e. skin color) face_id (uint8): the identifier that will determine what facial characteristics the npc will have hair_style_id (uint8): the identifier that will determine the style of hair the npc has facial_hair_id (uint8): the identifier that will determine what kind of facial-hair the npc will have item_displays (uint32): the display id of the item corresponding to the specific slot; the armor IDs reference into ItemDisplayInfo.dbc while the mainhand, offhand and ranged weapon IDs reference directly into Item.dbc
Essentially, when the NPC is spawned on the client, its entry is first looked up in this table. If the entry provided corresponds to one of the entries in the server_played_character table, the information in the server_played_character will override all of the npc's display information in the creature_spawns and/or the creature_info tables.
Low-Level Details:
When the player update opcode is sent from the server to the client and it updates an npc's FIELD_FLAGS_2 and bit 5 in that uint32 is set in the update packet but, for the specific unit, is cleared on the client and the unit's display ID is a player display ID (i.e. dwarf, human, gnome etc.) then the client sends a packet to the server (with opcode CMSG_GET_MIRRORIMAGE_DATA). At this time (and only this time), the server needs to respond with the proper data to service the request (with the SMSG_MIRRORIMAGE_DATA opcode). Attempting to send any mirror image data to the client when the npc's display ID is not of a native player type or when its MIRROR_IMAGE field flag is cleared will merely cause the npc to become "invisible."
I'll leave it to the scripters from here. Have fun and be sure to post any questions or suggestions below.
Updates:
I have added various races (although I'm not sure as to whether their client models are valid) and added support for female display IDs. In addition, it should be noted that the armor items in the SQL tables are references into the ItemDisplayInfo.dbc dbc file. However, the mainhand, offhand and ranged items are references into Item.dbc