Browse Source

- Fix #453 PlayFlavor re-design
PlayFlavorID(NPC, type, id, index, Player, language)
Set Player to 'nil' to send to all clients, specifying a Player will make it send ONLY to that player
New command /reload voiceovers added
- versioning updated to 0.9.4-aquarii
- Fix #454 Support for SendShowBook to have language, items field 'book_language' added
- Some debug log cleanup

Emagi 1 year ago
parent
commit
0e00165195

+ 1 - 0
DB/updates/jul22_items_book_language.sql

@@ -0,0 +1 @@
+alter table items add column book_language tinyint(3) unsigned default 0;

+ 13 - 0
DB/updates/voiceovers_table_july23_2022.sql

@@ -0,0 +1,13 @@
+insert into commands set type=1,command='reload',subcommand='voiceovers',required_status=100,handler=532;
+CREATE TABLE `voiceovers` (
+  `type_id` tinyint(3) unsigned NOT NULL default 0,
+  `id` int(10) unsigned NOT NULL default 0,
+  `indexed` smallint(5) unsigned NOT NULL default 0,
+  `mp3_string` text not null default '',
+  `text_string` text not null default '',
+  `emote_string` text not null default '',
+  `key1` int(10) unsigned NOT NULL default 0,
+  `key2` int(10) unsigned NOT NULL default 0,
+  `garbled` tinyint(3) unsigned NOT NULL default 0,
+  `garble_link_id` tinyint(3) unsigned NOT NULL default 0
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;

+ 2 - 2
EQ2/source/WorldServer/Bots/BotCommands.cpp

@@ -756,7 +756,7 @@ void Commands::Command_Bot_Help(Client* client, Seperator* sep) {
 			details += "18\tSarnak\n";
 			details += "19\tVampire\n";
 			details += "20\tAerakyn\n";
-			client->SendShowBook(client->GetPlayer(), title, 1, details);
+			client->SendShowBook(client->GetPlayer(), title, 0, 1, details);
 			return;
 		}
 		else if (strncasecmp("class", sep->arg[0], 5) == 0) {
@@ -811,7 +811,7 @@ void Commands::Command_Bot_Help(Client* client, Seperator* sep) {
 			details3 += "43\tSHAPER\n";
 			details3 += "44\tCHANNELER\n";
 
-			client->SendShowBook(client->GetPlayer(), title, 3, details, details2, details3);
+			client->SendShowBook(client->GetPlayer(), title, 0, 3, details, details2, details3);
 			return;
 		}
 	}

+ 11 - 3
EQ2/source/WorldServer/Commands/Commands.cpp

@@ -1833,6 +1833,7 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 		client->SimpleMessage(CHANNEL_COLOR_YELLOW, "/reload rules");
 		client->SimpleMessage(CHANNEL_COLOR_YELLOW, "/reload transporters");
 		client->SimpleMessage(CHANNEL_COLOR_YELLOW, "/reload startabilities");
+		client->SimpleMessage(CHANNEL_COLOR_YELLOW, "/reload voiceovers");
 		break;
 	}
 	case COMMAND_RELOADSTRUCTS: {
@@ -1946,6 +1947,13 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 		client->SimpleMessage(CHANNEL_COLOR_YELLOW, "Done!");
 		break;
 	}
+	case COMMAND_RELOAD_VOICEOVERS: {
+		client->SimpleMessage(CHANNEL_COLOR_YELLOW, "Reloading Voiceovers...");
+		world.PurgeVoiceOvers();
+		world.LoadVoiceOvers();
+		client->SimpleMessage(CHANNEL_COLOR_YELLOW, "Done!");
+		break;
+	}
 	case COMMAND_READ: {
 		if (sep && sep->arg[1][0] && sep->IsNumber(1)) {
 			if (strcmp(sep->arg[0], "read") == 0) {
@@ -1953,7 +1961,7 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 				Item* item = client->GetPlayer()->item_list.GetItemFromIndex(item_index);
 				if (item) {
 					Spawn* spawn = cmdTarget;
-					client->SendShowBook(client->GetPlayer(), item->name, item->book_pages);
+					client->SendShowBook(client->GetPlayer(), item->name, item->book_language, item->book_pages);
 					break;
 				}
 			}
@@ -3867,7 +3875,7 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 							client->GetCurrentZone()->SendAllSpawnsForVisChange(client, false);
 							client->Message(CHANNEL_COLOR_RED, "Adding spawn group tag \"%s\" with tag icon %u.", (value == 1) ? "on" : "off", tag_icon);
 						}
-						else if(strncasecmp(sep->arg[1], "race", 5) == 0){
+						else if(strncasecmp(sep->arg[1], "race", 4 == 0)){
 							if(!value) {
 								client->SimpleMessage(CHANNEL_COLOR_RED, "Need to supply a valid race id.");
 								break;
@@ -4742,7 +4750,7 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie
 				}
 
 				string title = string(spawn->GetName()) + "(" + to_string(spawn->GetDatabaseID()) + ")";
-				client->SendShowBook(client->GetPlayer(), title, 4, details, details2, details3, details4);
+				client->SendShowBook(client->GetPlayer(), title, 0, 4, details, details2, details3, details4);
 			}
 			else {
 				client->SimpleMessage(CHANNEL_COLOR_YELLOW, "Syntax: /spawn details (radius)");

+ 2 - 0
EQ2/source/WorldServer/Commands/Commands.h

@@ -932,6 +932,8 @@ private:
 #define COMMAND_CANCEL_EFFECT			530
 #define COMMAND_CUREPLAYER			531
 
+#define COMMAND_RELOAD_VOICEOVERS		532
+
 
 #define GET_AA_XML						750
 #define ADD_AA							751

+ 3 - 0
EQ2/source/WorldServer/Items/Items.cpp

@@ -897,6 +897,7 @@ Item::Item(){
 	no_sale = false;
 	created = std::time(nullptr);
 	effect_type = NO_EFFECT_TYPE;
+	book_language = 0;
 }
 
 Item::Item(Item* in_item){
@@ -917,6 +918,7 @@ Item::Item(Item* in_item){
 	created = in_item->created;
 	grouped_char_ids.insert(in_item->grouped_char_ids.begin(), in_item->grouped_char_ids.end());
 	effect_type = in_item->effect_type;
+	book_language = in_item->book_language;
 }
 
 Item::~Item(){
@@ -1140,6 +1142,7 @@ void Item::SetItem(Item* old_item){
 	slot_data = old_item->slot_data;
 	spell_id = old_item->spell_id;
 	spell_tier = old_item->spell_tier;
+	book_language = old_item->book_language;
 }
 
 bool Item::CheckArchetypeAdvSubclass(int8 adventure_class, map<int8, int16>* adv_class_levels) {

+ 1 - 0
EQ2/source/WorldServer/Items/Items.h

@@ -951,6 +951,7 @@ public:
 	ItemEffectType			effect_type;
 	bool 					crafted;
 	bool					tinkered;
+	int8					book_language;
 	
 	void AddEffect(string effect, int8 percentage, int8 subbulletflag);
 	void AddBookPage(int8 page, string page_text,int8 valign, int8 halign);

+ 1 - 0
EQ2/source/WorldServer/Items/ItemsDB.cpp

@@ -224,6 +224,7 @@ void WorldDatabase::LoadDataFromRow(DatabaseResult* result, Item* item)
 	
 	item->crafted = result->GetInt8Str("crafted");
 	item->tinkered = result->GetInt8Str("tinkered");
+	item->book_language = result->GetInt8Str("book_language");
 	
 }
 

+ 27 - 0
EQ2/source/WorldServer/LuaFunctions.cpp

@@ -188,6 +188,33 @@ int EQ2Emu_lua_PlayFlavor(lua_State* state) {
 	return 0;
 }
 
+int EQ2Emu_lua_PlayFlavorID(lua_State* state) {
+	if (!lua_interface)
+		return 0;
+	Spawn* spawn = lua_interface->GetSpawn(state);
+	
+	int8 type = lua_interface->GetInt8Value(state, 2);
+	int32 id = lua_interface->GetInt32Value(state, 3);
+	int16 index = lua_interface->GetInt16Value(state, 4);
+	Spawn* player = lua_interface->GetSpawn(state, 5);
+	int8 language = lua_interface->GetInt8Value(state, 6);
+	lua_interface->ResetFunctionStack(state);
+	if (spawn) {
+			Client* client = 0;
+			if (player && player->IsPlayer())
+				client = spawn->GetZone()->GetClientBySpawn(player);
+			if (client) {
+				VoiceOverStruct non_garble, garble;
+				bool garble_success = false;
+				bool success = world.FindVoiceOver(type, id, index, &non_garble, &garble_success, &garble);
+				client->SendPlayFlavor(spawn, language, &non_garble, &garble, success, garble_success);
+			}
+			else
+				spawn->GetZone()->PlayFlavorID(spawn, type, id, index, language);	
+	}
+	return 0;
+}
+
 int EQ2Emu_lua_PlaySound(lua_State* state) {
 	if (!lua_interface)
 		return 0;

+ 1 - 0
EQ2/source/WorldServer/LuaFunctions.h

@@ -194,6 +194,7 @@ int EQ2Emu_lua_GetCharacterID(lua_State* state);
 int EQ2Emu_lua_MovementLoopAdd(lua_State* state);
 int EQ2Emu_lua_GetCurrentZoneSafeLocation(lua_State* state);
 int EQ2Emu_lua_PlayFlavor(lua_State* state);
+int EQ2Emu_lua_PlayFlavorID(lua_State* state);
 int EQ2Emu_lua_PlaySound(lua_State* state);
 int EQ2Emu_lua_PlayVoice(lua_State* state);
 int EQ2Emu_lua_PlayAnimation(lua_State* state);

+ 1 - 0
EQ2/source/WorldServer/LuaInterface.cpp

@@ -1057,6 +1057,7 @@ void LuaInterface::RegisterFunctions(lua_State* state) {
 	lua_register(state, "GetSpawnByGroupID", EQ2Emu_lua_GetSpawnByGroupID);
 	lua_register(state, "GetSpawnByLocationID", EQ2Emu_lua_GetSpawnByLocationID);
 	lua_register(state, "PlayFlavor", EQ2Emu_lua_PlayFlavor);
+	lua_register(state, "PlayFlavorID", EQ2Emu_lua_PlayFlavorID);
 	lua_register(state, "PlaySound", EQ2Emu_lua_PlaySound);
 	lua_register(state, "PlayVoice", EQ2Emu_lua_PlayVoice);
 	lua_register(state, "PlayAnimation", EQ2Emu_lua_PlayAnimation);

+ 156 - 0
EQ2/source/WorldServer/World.cpp

@@ -132,6 +132,7 @@ World::~World(){
 	tov_itemstat_conversion.clear();
 
 	PurgeStartingLists();
+	PurgeVoiceOvers();
 
 	map<std::string, RegionMapRange*>::iterator itr3;
 	for (itr3 = region_maps.begin(); itr3 != region_maps.end(); itr3++)
@@ -170,6 +171,7 @@ void World::init(){
 	LogWrite(WORLD__DEBUG, 1, "World", "-Load `visual states` complete!");
 
 	LoadStartingLists();
+	LoadVoiceOvers();
 
 	LogWrite(WORLD__DEBUG, 1, "World", "-Setting system parameters...");
 	Variable* var = variables.FindVariable("gametime");
@@ -2411,7 +2413,18 @@ void World::PurgeStartingLists()
 		safe_delete(tmpMap);
 	}
 	starting_spells.clear();
+	
+
+	for(int type=0;type<3;type++) {
+		multimap<int32, multimap<int16, VoiceOverStruct>*>::iterator vos_itr;
 
+		for (vos_itr = voiceover_map[type].begin(); vos_itr != voiceover_map[type].end(); vos_itr++)
+		{
+			multimap<int16, VoiceOverStruct>* tmpMap = vos_itr->second;
+			safe_delete(tmpMap);
+		}
+		voiceover_map[type].clear();
+	}
 	MStartingLists.releasewritelock();
 }
 
@@ -2611,3 +2624,146 @@ void World::SendTimeUpdate()
 {
 	zone_list.SendTimeUpdate();
 }
+
+void World::LoadVoiceOvers()
+{
+	LogWrite(WORLD__DEBUG, 1, "World", "-Loading `voiceovers`...");
+	database.LoadVoiceOvers(this);
+}
+
+
+void World::PurgeVoiceOvers()
+{
+	MVoiceOvers.writelock();
+	for(int type=0;type<MAX_VOICEOVER_TYPE+1;type++) {
+		multimap<int32, multimap<int16, VoiceOverStruct>*>::iterator vos_itr;
+
+		for (vos_itr = voiceover_map[type].begin(); vos_itr != voiceover_map[type].end(); vos_itr++)
+		{
+			multimap<int16, VoiceOverStruct>* tmpMap = vos_itr->second;
+			safe_delete(tmpMap);
+		}
+		voiceover_map[type].clear();
+	}
+	MVoiceOvers.releasewritelock();
+}
+
+
+bool World::FindVoiceOver(int8 type, int32 id, int16 index, VoiceOverStruct* struct_, bool* find_garbled, VoiceOverStruct* garble_struct_) {
+	// if we complete both requirements, based on struct_ and garble_struct_  being passed when required by ptr not being null
+	bool succeed = false;
+	if(type > MAX_VOICEOVER_TYPE) {
+		LogWrite(WORLD__ERROR, 0, "World", "Voice over %u out of range, max voiceover type is %u...", type, MAX_VOICEOVER_TYPE);
+		return succeed;
+	}
+	
+	MVoiceOvers.readlock();
+	multimap<int32, multimap<int16, VoiceOverStruct>*>::iterator itr = voiceover_map[type].find(id);
+	if(itr != voiceover_map[type].end()) {
+			std::pair<VOMapIterator, VOMapIterator> result = itr->second->equal_range(index);
+			int count = std::distance(result.first, result.second);
+			bool tries_attempt = true; // abort out the while loop
+			bool non_garble_found = false;
+			int rand = 0; // use to randomize the voiceover selection
+			int pos = 0;
+			int tries = 0;
+			bool has_ungarbled = false;
+			bool has_garbled = false;
+			int8 garble_link_id = 0; // used to match ungarbled to garbled when the link id is set in the DB
+			while(tries_attempt) {
+				pos = 0;
+				rand = MakeRandomInt(0, count);
+				if ( tries == 3 || non_garble_found || (find_garbled && (*find_garbled)))
+					rand = 0; // override, too many tries, or we otherwise found one garbled/ungarbled lets try to link it
+				for (VOMapIterator it = result.first; it != result.second; it++) {
+					if(!it->second.is_garbled) {
+						has_ungarbled = true;
+					}
+					else {
+						has_garbled = true;
+					}
+					pos++;
+					
+					// if there is only 1 entry in the voiceover list we aren't going to bother skipping
+					if(count > 1 && pos < rand) {
+						continue;
+					}
+					if(!it->second.is_garbled && (garble_link_id == 0 || it->second.garble_link_id == garble_link_id)) {
+						garble_link_id = it->second.garble_link_id;
+						non_garble_found = true;
+						if(struct_) {
+							CopyVoiceOver(struct_, &it->second);
+						}
+						
+						if(!find_garbled || ((*find_garbled))) {
+							if(find_garbled)
+								*find_garbled = true;
+							tries_attempt = false;
+							succeed = true;
+							break;
+						}
+					}
+					else if(find_garbled && !(*find_garbled) && it->second.is_garbled && (garble_link_id == 0 || it->second.garble_link_id == garble_link_id)) { 
+						*find_garbled = true;
+						garble_link_id = it->second.garble_link_id;
+						if(garble_struct_) {
+							CopyVoiceOver(garble_struct_, &it->second);
+							if(!struct_ || non_garble_found) {
+								tries_attempt = false;
+								succeed = true;
+								break;
+							}
+						}
+					}
+				}
+				tries++;
+				if(!tries_attempt || (tries > 0 && !has_ungarbled && (!find_garbled || *find_garbled == true || !has_garbled)) || tries > 3)
+					break;
+			}
+	}
+	MVoiceOvers.releasereadlock();
+	
+	return succeed;
+}
+
+void World::AddVoiceOver(int8 type, int32 id, int16 index, VoiceOverStruct* struct_) {
+	if(type > MAX_VOICEOVER_TYPE) {
+		LogWrite(WORLD__ERROR, 0, "World", "Voice over %u out of range, max voiceover type is %u...", type, MAX_VOICEOVER_TYPE);
+		return;
+	}
+	
+	VoiceOverStruct tmpStruct;
+	tmpStruct.mp3_string = std::string(struct_->mp3_string);
+	tmpStruct.text_string = std::string(struct_->text_string);
+	tmpStruct.emote_string = std::string(struct_->emote_string);
+	tmpStruct.key1 = struct_->key1;
+	tmpStruct.key2 = struct_->key2;
+	tmpStruct.is_garbled = struct_->is_garbled;
+	
+	MVoiceOvers.writelock();
+	if(!voiceover_map[type].count(id))
+	{
+		multimap<int16, VoiceOverStruct>* vo_struct = new multimap<int16, VoiceOverStruct>();
+		vo_struct->insert(make_pair(index, tmpStruct));
+		voiceover_map[type].insert(make_pair(id, vo_struct));
+	}
+	else
+	{
+		multimap<int32, multimap<int16, VoiceOverStruct>*>::const_iterator itr = voiceover_map[type].find(id);
+		itr->second->insert(make_pair(index, tmpStruct));
+	}
+	MVoiceOvers.releasewritelock();
+}
+
+void World::CopyVoiceOver(VoiceOverStruct* struct1, VoiceOverStruct* struct2) {
+	if(!struct1 || !struct2)
+		return;
+	
+	struct1->mp3_string = std::string(struct2->mp3_string);
+	struct1->text_string = std::string(struct2->text_string);
+	struct1->emote_string = std::string(struct2->emote_string);
+	struct1->key1 = struct2->key1;
+	struct1->key2 = struct2->key2;
+	struct1->is_garbled = struct2->is_garbled;
+	struct1->garble_link_id = struct2->garble_link_id;
+}

+ 20 - 2
EQ2/source/WorldServer/World.h

@@ -398,6 +398,17 @@ struct StartingSpell
 	int32 knowledge_slot;
 };
 
+#define MAX_VOICEOVER_TYPE 2
+struct VoiceOverStruct{
+	string				mp3_string;
+	string				text_string;
+	string				emote_string;
+	int32				key1;
+	int32				key2;
+	bool				is_garbled;
+	int8				garble_link_id;
+};
+
 class ZoneList {
 	public:
 	ZoneList();
@@ -642,10 +653,17 @@ public:
 	// just in case we roll over a time as to not send bad times to clients (days before hours, hours before minutes as examples)
 	Mutex MWorldTime;
 
-
-
+	void LoadVoiceOvers();
+	void PurgeVoiceOvers();
+	typedef std::multimap<int16, VoiceOverStruct>::iterator VOMapIterator;
+	bool FindVoiceOver(int8 type, int32 id, int16 index, VoiceOverStruct* struct_ = nullptr, bool* find_garbled = nullptr, VoiceOverStruct* garble_struct_ = nullptr);
+	void AddVoiceOver(int8 type, int32 id, int16 index, VoiceOverStruct* struct_);
+	void CopyVoiceOver(VoiceOverStruct* struct1, VoiceOverStruct* struct2);
+	Mutex MVoiceOvers;
+	
 	static sint64 newValue;
 private:
+	multimap<int32, multimap<int16, VoiceOverStruct>*> voiceover_map[3];
 	int32 suppressed_warning = 0;
 	map<string, int32> reloading_subsystems;
 	//void RemovePlayerFromGroup(PlayerGroup* group, GroupMemberInfo* info, bool erase = true);

+ 36 - 0
EQ2/source/WorldServer/WorldDatabase.cpp

@@ -7416,6 +7416,42 @@ void WorldDatabase::LoadStartingSkills(World* world)
 }
 
 
+void WorldDatabase::LoadVoiceOvers(World* world)
+{
+	int32 total = 0;
+	Query query;
+	MYSQL_ROW row;
+	MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT type_id, id, indexed, mp3_string, text_string, emote_string, key1, key2, garbled, garble_link_id FROM voiceovers");
+
+	if (result)
+	{
+		if (mysql_num_rows(result) > 0)
+		{
+			Skill* skill = 0;
+
+			while (result && (row = mysql_fetch_row(result)))
+			{
+
+				VoiceOverStruct vos;
+				vos.mp3_string = std::string(row[3]);
+				vos.text_string = std::string(row[4]);
+				vos.emote_string = std::string(row[5]);
+				vos.key1 = atoul(row[6]);
+				vos.key2 = atoul(row[7]);
+				vos.is_garbled = atoul(row[8]);
+				vos.garble_link_id = atoul(row[9]);
+				int8 type = atoul(row[0]);
+				int32 id = atoul(row[1]);
+				int16 index = atoul(row[2]);
+				world->AddVoiceOver(type, id, index, &vos);
+				total++;
+			}
+		}
+	}
+	LogWrite(WORLD__DEBUG, 3, "World", "--Loaded %u Voiceover(s)", total);
+}
+
+
 void WorldDatabase::LoadStartingSpells(World* world)
 {
 	world->MStartingLists.writelock();

+ 1 - 0
EQ2/source/WorldServer/WorldDatabase.h

@@ -604,6 +604,7 @@ public:
 
 	void				LoadStartingSkills(World* world);
 	void				LoadStartingSpells(World* world);
+	void				LoadVoiceOvers(World* world);
 
 	int32				CreateSpiritShard(const char* name, int32 level, int8 race, int8 gender, int8 adventure_class, 
 									  int16 model_type, int16 soga_model_type, int16 hair_type, int16 hair_face_type, int16 wing_type,

+ 0 - 1
EQ2/source/WorldServer/Zone/region_map_v1.cpp

@@ -64,7 +64,6 @@ std::string RegionMapV1::TestFile(std::string testFile)
 	string tmpScript("RegionScripts/");
 	tmpScript.append(mZoneNameLower);
 	tmpScript.append("/" + tmpStr + ".lua");
-	printf("File to test : %s\n",tmpScript.c_str());
 	std::ifstream f(tmpScript.c_str());
 	return f.good() ? tmpScript : string("");
 }

+ 26 - 3
EQ2/source/WorldServer/client.cpp

@@ -10530,7 +10530,7 @@ void Client::SendFlightAutoMount(int32 path_id, int16 mount_id, int8 mount_red_c
 		((Entity*)GetPlayer())->SetMount(mount_id, mount_red_color, mount_green_color, mount_blue_color);
 }
 
-void Client::SendShowBook(Spawn* sender, string title, int8 num_pages, ...)
+void Client::SendShowBook(Spawn* sender, string title, int8 language, int8 num_pages, ...)
 {
 	if (!sender)
 	{
@@ -10547,7 +10547,9 @@ void Client::SendShowBook(Spawn* sender, string title, int8 num_pages, ...)
 	packet->setDataByName("book_title", title.c_str());
 	packet->setDataByName("book_type", "simple");
 	packet->setDataByName("unknown2", 1);
-
+	if(language > 0 && !GetPlayer()->HasLanguage(language))
+		packet->setDataByName("language", language);
+	
 	if (GetVersion() > 546)
 		packet->setDataByName("unknown5", 1, 4);
 
@@ -10596,7 +10598,7 @@ void Client::SendShowBook(Spawn* sender, string title, int8 num_pages, ...)
 	safe_delete(packet);
 }
 
-void Client::SendShowBook(Spawn* sender, string title, vector<Item::BookPage*> pages)
+void Client::SendShowBook(Spawn* sender, string title, int8 language, vector<Item::BookPage*> pages)
 {
 	if (!sender)
 	{
@@ -10613,6 +10615,9 @@ void Client::SendShowBook(Spawn* sender, string title, vector<Item::BookPage*> p
 	packet->setDataByName("book_title", title.c_str());
 	packet->setDataByName("book_type", "simple");
 	packet->setDataByName("unknown2", 1);
+	
+	if(language > 0 && !GetPlayer()->HasLanguage(language))
+		packet->setDataByName("language", language);
 
 	if (GetVersion() > 546)
 		packet->setDataByName("unknown5", 1, 4);
@@ -11018,4 +11023,22 @@ bool Client::UseItem(Item* item, Spawn* target) {
 		}
 	}
 	return false;
+}
+
+void Client::SendPlayFlavor(Spawn* spawn, int8 language, VoiceOverStruct* non_garble, 
+								VoiceOverStruct* garble, bool success, bool garble_success) {
+		VoiceOverStruct* resStruct = nullptr;
+		
+		if(language == 0 || GetPlayer()->HasLanguage(language)) {
+			if(success) {
+				resStruct = non_garble;
+			}
+		}
+		else if(garble_success) {
+			resStruct = garble;
+		}
+		
+		if(resStruct) {
+			GetPlayer()->GetZone()->PlayFlavor(this, spawn, resStruct->mp3_string.c_str(), resStruct->text_string.c_str(), resStruct->emote_string.c_str(), resStruct->key1, resStruct->key2, language);
+		}
 }

+ 5 - 2
EQ2/source/WorldServer/client.h

@@ -32,6 +32,7 @@ using namespace std;
 #define CLIENT_TIMEOUT 60000
 struct TransportDestination;
 struct ConversationOption;
+struct VoiceOverStruct;
 
 #define MAIL_SEND_RESULT_SUCCESS				0
 #define MAIL_SEND_RESULT_UNKNOWN_PLAYER			1
@@ -446,8 +447,8 @@ public:
 
 	void SendFlightAutoMount(int32 path_id, int16 mount_id = 0, int8 mount_red_color = 0xFF, int8 mount_green_color = 0xFF, int8 mount_blue_color=0xFF);
 
-	void SendShowBook(Spawn* sender, string title, int8 num_pages, ...);
-	void SendShowBook(Spawn* sender, string title, vector<Item::BookPage*> pages);
+	void SendShowBook(Spawn* sender, string title, int8 language, int8 num_pages, ...);
+	void SendShowBook(Spawn* sender, string title, int8 language, vector<Item::BookPage*> pages);
 
 	void SetTemporaryTransportID(int32 id) { temporary_transport_id = id; }
 	int32 GetTemporaryTransportID() { return temporary_transport_id; }
@@ -540,6 +541,8 @@ public:
 	}
 	
 	bool	UseItem(Item* item, Spawn* target = nullptr);
+	
+	void	SendPlayFlavor(Spawn* spawn, int8 language, VoiceOverStruct* non_garble, VoiceOverStruct* garble, bool success = false, bool garble_success = false);
 private:
 	void    SavePlayerImages();
 	void	SkillChanged(Skill* skill, int16 previous_value, int16 new_value);

+ 23 - 0
EQ2/source/WorldServer/zoneserver.cpp

@@ -3654,6 +3654,29 @@ void ZoneServer::PlayFlavor(Spawn* spawn, const char* mp3, const char* text, con
 	MClientList.releasereadlock(__FUNCTION__, __LINE__);
 }
 
+void ZoneServer::PlayFlavorID(Spawn* spawn, int8 type, int32 id, int16 index, int8 language){
+	if(!spawn)
+		return;
+
+	Client* client = 0;
+	vector<Client*>::iterator client_itr;
+
+	VoiceOverStruct non_garble, garble;
+	bool garble_success = false;
+	bool success = world.FindVoiceOver(type, id, index, &non_garble, &garble_success, &garble);
+	
+	VoiceOverStruct* resStruct = nullptr;
+	MClientList.readlock(__FUNCTION__, __LINE__);
+	for (client_itr = clients.begin(); client_itr != clients.end(); client_itr++) {
+		client = *client_itr;
+		if(!client || !client->IsReadyForUpdates() || !client->GetPlayer()->WasSentSpawn(spawn->GetID()) || client->GetPlayer()->GetDistance(spawn) > 30)
+			continue;
+		
+		client->SendPlayFlavor(spawn, language, &non_garble, &garble, success, garble_success);
+	}
+	MClientList.releasereadlock(__FUNCTION__, __LINE__);
+}
+
 void ZoneServer::PlayVoice(Spawn* spawn, const char* mp3, int32 key1, int32 key2){
 	if(!spawn || !mp3)
 		return;

+ 1 - 0
EQ2/source/WorldServer/zoneserver.h

@@ -450,6 +450,7 @@ public:
 	void			PlayFlavor(Client* client, Spawn* spawn, const char* mp3, const char* text, const char* emote, int32 key1, int32 key2, int8 language);
 	void			PlayVoice(Client* client, Spawn* spawn, const char* mp3, int32 key1, int32 key2);
 	void			PlayFlavor(Spawn* spawn, const char* mp3, const char* text, const char* emote, int32 key1, int32 key2, int8 language);
+	void			PlayFlavorID(Spawn* spawn, int8 type, int32 id, int16 index, int8 language);
 	void			PlayVoice(Spawn* spawn, const char* mp3, int32 key1, int32 key2);
 	void			SendThreatPacket(Spawn* caster, Spawn* target, int32 threat_amt, const char* spell_name);
 	void			KillSpawnByDistance(Spawn* spawn, float max_distance, bool include_players = false, bool send_packet = false);

+ 3 - 3
EQ2/source/common/version.h

@@ -38,11 +38,11 @@
 #endif
 
 #if defined(LOGIN)
-#define CURRENT_VERSION	"0.9.4-geminorum"
+#define CURRENT_VERSION	"0.9.4-aquarii"
 #elif defined(WORLD)
-#define CURRENT_VERSION	"0.9.4-geminorum"
+#define CURRENT_VERSION	"0.9.4-aquarii"
 #else
-#define CURRENT_VERSION	"0.9.4-geminorum"
+#define CURRENT_VERSION	"0.9.4-aquarii"
 #endif
 
 #define COMPILE_DATE	__DATE__

+ 4 - 2
server/WorldStructs.xml

@@ -9876,7 +9876,8 @@ to zero and treated like placeholders." />
 <Struct Name="WS_EqShowBook" ClientVersion="546" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_EqShowBookCmd" >
 <Data ElementName="spawn_id" Type="int32" Size="1" />
 <Data ElementName="book_title" Type="EQ2_16Bit_String" Size="1" />
-<Data ElementName="unknown" Type="int16" Size="1" />
+<Data ElementName="language" Type="int8" Size="1" />
+<Data ElementName="unknown1" Type="int8" Size="1" />
 <Data ElementName="book_type" Type="EQ2_16Bit_String" Size="1" />
 <Data ElementName="unknown2" Type="int16" Size="1" />
 <Data ElementName="num_pages" Type="int8" Size="1" />
@@ -9891,7 +9892,8 @@ to zero and treated like placeholders." />
 <Struct Name="WS_EqShowBook" ClientVersion="1096" OpcodeName="OP_ClientCmdMsg" OpcodeType="OP_EqShowBookCmd" >
 <Data ElementName="spawn_id" Type="int32" Size="1" />
 <Data ElementName="book_title" Type="EQ2_16Bit_String" Size="1" />
-<Data ElementName="unknown" Type="int16" Size="1" />
+<Data ElementName="language" Type="int8" Size="1" />
+<Data ElementName="unknown1" Type="int8" Size="1" />
 <Data ElementName="book_type" Type="EQ2_16Bit_String" Size="1" />
 <Data ElementName="unknown2" Type="int8" Size="1" />
 <Data ElementName="unknown3" Type="int16" Size="1" />