diff --git a/common/database/database_update_manifest.cpp b/common/database/database_update_manifest.cpp index c272b53b4..29aad812f 100644 --- a/common/database/database_update_manifest.cpp +++ b/common/database/database_update_manifest.cpp @@ -7087,6 +7087,18 @@ CREATE INDEX `idx_expire_at` ON `instance_list` (`expire_at`); )", .content_schema_update = false }, + ManifestEntry{ + .version = 9322, + .description = "2025_04_24_add_npc_tint_id.sql", + .check = "SHOW COLUMNS FROM `npc_types` LIKE 'npc_tint_id'", + .condition = "empty", + .match = "", + .sql = R"( +ALTER TABLE `npc_types` + ADD COLUMN `npc_tint_id` SMALLINT UNSIGNED NULL DEFAULT '0' AFTER `multiquest_enabled`; +)", + .content_schema_update = true + } // -- template; copy/paste this when you need to create a new entry // ManifestEntry{ // .version = 9228, diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index ef4d376e6..0a1349254 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -325,6 +325,7 @@ union bool trader; bool buyer; bool untargetable; + uint32 npc_tint_id; }; struct PlayerState_Struct { diff --git a/common/patches/rof2.cpp b/common/patches/rof2.cpp index 1fcfcf67a..747d68204 100644 --- a/common/patches/rof2.cpp +++ b/common/patches/rof2.cpp @@ -4839,13 +4839,13 @@ namespace RoF2 VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->petOwnerId); - VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // FindBits MQ2 name + VARSTRUCT_ENCODE_TYPE(uint8, Buffer, 0); // FindBits MQ2 name VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->PlayerState); - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // NpcTintIndex - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // PrimaryTintIndex - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // SecondaryTintIndex - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0xffffffff); // These do something with OP_WeaponEquip1 - VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0xffffffff); // ^ + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, emu->npc_tint_id); // NpcTintIndex + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // PrimaryTintIndex + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0); // SecondaryTintIndex + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0xffffffff); // These do something with OP_WeaponEquip1 + VARSTRUCT_ENCODE_TYPE(uint32, Buffer, 0xffffffff); // ^ if ((emu->NPC == 0) || (emu->race <= Race::Gnome) || (emu->race == Race::Iksar) || (emu->race == Race::VahShir) || (emu->race == Race::Froglok2) || (emu->race == Race::Drakkin) diff --git a/common/repositories/base/base_npc_types_repository.h b/common/repositories/base/base_npc_types_repository.h index e8ed23c8e..f6019e987 100644 --- a/common/repositories/base/base_npc_types_repository.h +++ b/common/repositories/base/base_npc_types_repository.h @@ -149,6 +149,7 @@ public: uint8_t keeps_sold_items; uint8_t is_parcel_merchant; uint8_t multiquest_enabled; + uint16_t npc_tint_id; }; static std::string PrimaryKey() @@ -289,6 +290,7 @@ public: "keeps_sold_items", "is_parcel_merchant", "multiquest_enabled", + "npc_tint_id", }; } @@ -425,6 +427,7 @@ public: "keeps_sold_items", "is_parcel_merchant", "multiquest_enabled", + "npc_tint_id", }; } @@ -595,6 +598,7 @@ public: e.keeps_sold_items = 1; e.is_parcel_merchant = 0; e.multiquest_enabled = 0; + e.npc_tint_id = 0; return e; } @@ -761,6 +765,7 @@ public: e.keeps_sold_items = row[127] ? static_cast(strtoul(row[127], nullptr, 10)) : 1; e.is_parcel_merchant = row[128] ? static_cast(strtoul(row[128], nullptr, 10)) : 0; e.multiquest_enabled = row[129] ? static_cast(strtoul(row[129], nullptr, 10)) : 0; + e.npc_tint_id = row[130] ? static_cast(strtoul(row[130], nullptr, 10)) : 0; return e; } @@ -923,6 +928,7 @@ public: v.push_back(columns[127] + " = " + std::to_string(e.keeps_sold_items)); v.push_back(columns[128] + " = " + std::to_string(e.is_parcel_merchant)); v.push_back(columns[129] + " = " + std::to_string(e.multiquest_enabled)); + v.push_back(columns[130] + " = " + std::to_string(e.npc_tint_id)); auto results = db.QueryDatabase( fmt::format( @@ -1074,6 +1080,7 @@ public: v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.is_parcel_merchant)); v.push_back(std::to_string(e.multiquest_enabled)); + v.push_back(std::to_string(e.npc_tint_id)); auto results = db.QueryDatabase( fmt::format( @@ -1233,6 +1240,7 @@ public: v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.is_parcel_merchant)); v.push_back(std::to_string(e.multiquest_enabled)); + v.push_back(std::to_string(e.npc_tint_id)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } @@ -1396,6 +1404,7 @@ public: e.keeps_sold_items = row[127] ? static_cast(strtoul(row[127], nullptr, 10)) : 1; e.is_parcel_merchant = row[128] ? static_cast(strtoul(row[128], nullptr, 10)) : 0; e.multiquest_enabled = row[129] ? static_cast(strtoul(row[129], nullptr, 10)) : 0; + e.npc_tint_id = row[130] ? static_cast(strtoul(row[130], nullptr, 10)) : 0; all_entries.push_back(e); } @@ -1550,6 +1559,7 @@ public: e.keeps_sold_items = row[127] ? static_cast(strtoul(row[127], nullptr, 10)) : 1; e.is_parcel_merchant = row[128] ? static_cast(strtoul(row[128], nullptr, 10)) : 0; e.multiquest_enabled = row[129] ? static_cast(strtoul(row[129], nullptr, 10)) : 0; + e.npc_tint_id = row[130] ? static_cast(strtoul(row[130], nullptr, 10)) : 0; all_entries.push_back(e); } @@ -1754,6 +1764,7 @@ public: v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.is_parcel_merchant)); v.push_back(std::to_string(e.multiquest_enabled)); + v.push_back(std::to_string(e.npc_tint_id)); auto results = db.QueryDatabase( fmt::format( @@ -1906,6 +1917,7 @@ public: v.push_back(std::to_string(e.keeps_sold_items)); v.push_back(std::to_string(e.is_parcel_merchant)); v.push_back(std::to_string(e.multiquest_enabled)); + v.push_back(std::to_string(e.npc_tint_id)); insert_chunks.push_back("(" + Strings::Implode(",", v) + ")"); } diff --git a/common/version.h b/common/version.h index 5d0b6048b..5a4bf1632 100644 --- a/common/version.h +++ b/common/version.h @@ -42,7 +42,7 @@ * Manifest: https://github.com/EQEmu/Server/blob/master/utils/sql/db_update_manifest.txt */ -#define CURRENT_BINARY_DATABASE_VERSION 9321 +#define CURRENT_BINARY_DATABASE_VERSION 9322 #define CURRENT_BINARY_BOTS_DATABASE_VERSION 9054 #endif diff --git a/zone/gm_commands/npcedit.cpp b/zone/gm_commands/npcedit.cpp index a7ee685e2..54315070e 100755 --- a/zone/gm_commands/npcedit.cpp +++ b/zone/gm_commands/npcedit.cpp @@ -1693,6 +1693,24 @@ void command_npcedit(Client *c, const Seperator *sep) c->Message(Chat::White, "Usage: #npcedit set_grid [Grid ID] - Sets an NPC's Grid ID"); return; } + } else if (!strcasecmp(sep->arg[1], "npc_tint_id")) { + if (sep->IsNumber(2)) { + const uint32 npc_tint_id = (Strings::ToUnsignedInt(sep->arg[2])); + + n.npc_tint_id = npc_tint_id; + + d = fmt::format( + "Set NPCTintID {} for {}", + npc_tint_id, + npc_id_string + ); + } else { + c->Message( + Chat::White, + "Usage: #npcedit npc_tint_id [id] - Sets an NPC's NPCTintID [0 - 78 for RoF2]" + ); + return; + } } else { SendNPCEditSubCommands(c); return; diff --git a/zone/lua_npc.cpp b/zone/lua_npc.cpp index 99123936d..67f26155d 100644 --- a/zone/lua_npc.cpp +++ b/zone/lua_npc.cpp @@ -945,6 +945,12 @@ bool Lua_NPC::IsResumedFromZoneSuspend() return self->IsResumedFromZoneSuspend(); } +void Lua_NPC::SetNPCTintIndex(uint32 id) +{ + Lua_Safe_Call_Void(); + self->SendAppearancePacket(AppearanceType::NPCTintIndex, id); +} + luabind::scope lua_register_npc() { return luabind::class_("NPC") .def(luabind::constructor<>()) @@ -1091,6 +1097,7 @@ luabind::scope lua_register_npc() { .def("SetLDoNTrapType", (void(Lua_NPC::*)(uint8))&Lua_NPC::SetLDoNTrapType) .def("SetNPCAggro", (void(Lua_NPC::*)(bool))&Lua_NPC::SetNPCAggro) .def("SetNPCFactionID", (void(Lua_NPC::*)(int))&Lua_NPC::SetNPCFactionID) + .def("SetNPCTintIndex", &Lua_NPC::SetNPCTintIndex) .def("SetPetSpellID", (void(Lua_NPC::*)(int))&Lua_NPC::SetPetSpellID) .def("SetPlatinum", (void(Lua_NPC::*)(uint32))&Lua_NPC::SetPlatinum) .def("SetPrimSkill", (void(Lua_NPC::*)(int))&Lua_NPC::SetPrimSkill) diff --git a/zone/lua_npc.h b/zone/lua_npc.h index 3e7ede475..90d6e2142 100644 --- a/zone/lua_npc.h +++ b/zone/lua_npc.h @@ -199,6 +199,8 @@ public: void ReturnHandinItems(Lua_Client c); Lua_Spawn GetSpawn(lua_State* L); bool IsResumedFromZoneSuspend(); + void SetNPCTintIndex(uint32 id); + }; #endif diff --git a/zone/mob.cpp b/zone/mob.cpp index 3b27482e2..0a3baee13 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -101,7 +101,8 @@ Mob::Mob( bool in_always_aggro, int32 in_heroic_strikethrough, bool in_keeps_sold_items, - int64 in_hp_regen_per_second + int64 in_hp_regen_per_second, + uint32 npc_tint_id ) : attack_timer(2000), attack_dw_timer(2000), @@ -289,6 +290,7 @@ Mob::Mob( always_aggro = in_always_aggro; heroic_strikethrough = in_heroic_strikethrough; keeps_sold_items = in_keeps_sold_items; + m_npc_tint_id = npc_tint_id; InitializeBuffSlots(); @@ -1285,23 +1287,24 @@ void Mob::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) strn0cpy(ns->spawn.lastName, lastname, sizeof(ns->spawn.lastName)); } - ns->spawn.heading = FloatToEQ12(m_Position.w); - ns->spawn.x = FloatToEQ19(m_Position.x);//((int32)x_pos)<<3; - ns->spawn.y = FloatToEQ19(m_Position.y);//((int32)y_pos)<<3; - ns->spawn.z = FloatToEQ19(m_Position.z);//((int32)z_pos)<<3; - ns->spawn.spawnId = GetID(); - ns->spawn.curHp = static_cast(GetHPRatio()); - ns->spawn.max_hp = 100; //this field needs a better name - ns->spawn.race = (use_model) ? use_model : race; - ns->spawn.runspeed = runspeed; - ns->spawn.walkspeed = walkspeed; - ns->spawn.class_ = class_; - ns->spawn.gender = gender; - ns->spawn.level = level; - ns->spawn.PlayerState = GetPlayerState(); - ns->spawn.deity = deity; - ns->spawn.animation = 0; - ns->spawn.findable = findable?1:0; + ns->spawn.heading = FloatToEQ12(m_Position.w); + ns->spawn.x = FloatToEQ19(m_Position.x); //((int32)x_pos)<<3; + ns->spawn.y = FloatToEQ19(m_Position.y); //((int32)y_pos)<<3; + ns->spawn.z = FloatToEQ19(m_Position.z); //((int32)z_pos)<<3; + ns->spawn.spawnId = GetID(); + ns->spawn.curHp = static_cast(GetHPRatio()); + ns->spawn.max_hp = 100; // this field needs a better name + ns->spawn.race = (use_model) ? use_model : race; + ns->spawn.runspeed = runspeed; + ns->spawn.walkspeed = walkspeed; + ns->spawn.class_ = class_; + ns->spawn.gender = gender; + ns->spawn.level = level; + ns->spawn.PlayerState = GetPlayerState(); + ns->spawn.deity = deity; + ns->spawn.animation = 0; + ns->spawn.findable = findable ? 1 : 0; + ns->spawn.npc_tint_id = GetNpcTintId(); UpdateActiveLight(); ns->spawn.light = m_Light.Type[EQ::lightsource::LightActive]; diff --git a/zone/mob.h b/zone/mob.h index 8664aca76..22a7a57ee 100644 --- a/zone/mob.h +++ b/zone/mob.h @@ -192,7 +192,8 @@ public: bool in_always_aggros_foes, int32 in_heroic_strikethrough, bool keeps_sold_items, - int64 in_hp_regen_per_second = 0 + int64 in_hp_regen_per_second = 0, + uint32 npc_tint_id = 0 ); virtual ~Mob(); @@ -1066,6 +1067,7 @@ public: void SendWearChangeAndLighting(int8 last_texture); inline uint8 GetActiveLightType() { return m_Light.Type[EQ::lightsource::LightActive]; } bool UpdateActiveLight(); // returns true if change, false if no change + uint32 GetNpcTintId() { return m_npc_tint_id; } EQ::LightSourceProfile* GetLightProfile() { return &m_Light; } @@ -1597,6 +1599,7 @@ protected: bool rare_spawn; int32 heroic_strikethrough; bool keeps_sold_items; + uint32 m_npc_tint_id; uint32 m_PlayerState; uint32 GetPlayerState() { return m_PlayerState; } diff --git a/zone/npc.cpp b/zone/npc.cpp index 1360d89a1..defebff94 100644 --- a/zone/npc.cpp +++ b/zone/npc.cpp @@ -128,7 +128,8 @@ NPC::NPC(const NPCType *npc_type_data, Spawn2 *in_respawn, const glm::vec4 &posi npc_type_data->always_aggro, npc_type_data->heroic_strikethrough, npc_type_data->keeps_sold_items, - npc_type_data->hp_regen_per_second + npc_type_data->hp_regen_per_second, + npc_type_data->m_npc_tint_id ), attacked_timer(CombatEventTimer_expire), swarm_timer(100), @@ -451,6 +452,7 @@ NPC::NPC(const NPCType *npc_type_data, Spawn2 *in_respawn, const glm::vec4 &posi raid_target = npc_type_data->raid_target; ignore_despawn = npc_type_data->ignore_despawn; m_targetable = !npc_type_data->untargetable; + m_npc_tint_id = npc_type_data->m_npc_tint_id; npc_scale_manager->ScaleNPC(this); @@ -1256,10 +1258,11 @@ uint32 ZoneDatabase::CreateNewNPCCommand( e.Avoidance = n->GetAvoidanceRating(); e.heroic_strikethrough = n->GetHeroicStrikethrough(); - e.see_hide = n->SeeHide(); - e.see_improved_hide = n->SeeImprovedHide(); - e.see_invis = n->SeeInvisible(); - e.see_invis_undead = n->SeeInvisibleUndead(); + e.see_hide = n->SeeHide(); + e.see_improved_hide = n->SeeImprovedHide(); + e.see_invis = n->SeeInvisible(); + e.see_invis_undead = n->SeeInvisibleUndead(); + e.npc_tint_id = n->GetNpcTintId(); e = NpcTypesRepository::InsertOne(*this, e); @@ -1399,6 +1402,7 @@ uint32 ZoneDatabase::UpdateNPCTypeAppearance(Client* c, NPC* n) e.loottable_id = n->GetLoottableID(); e.merchant_id = n->MerchantType; e.face = n->GetLuclinFace(); + e.npc_tint_id = n->GetNpcTintId(); const int updated = NpcTypesRepository::UpdateOne(*this, e); @@ -1539,6 +1543,7 @@ uint32 ZoneDatabase::AddNPCTypes( e.runspeed = n->GetRunspeed(); e.prim_melee_type = static_cast(EQ::skills::SkillHandtoHand); e.sec_melee_type = static_cast(EQ::skills::SkillHandtoHand); + e.npc_tint_id = n->GetNpcTintId(); e = NpcTypesRepository::InsertOne(*this, e); @@ -2169,9 +2174,10 @@ void NPC::FillSpawnStruct(NewSpawn_Struct* ns, Mob* ForWho) PetOnSpawn(ns); ns->spawn.is_npc = 1; UpdateActiveLight(); - ns->spawn.light = GetActiveLightType(); - ns->spawn.show_name = NPCTypedata->show_name; - ns->spawn.trader = false; + ns->spawn.light = GetActiveLightType(); + ns->spawn.show_name = NPCTypedata->show_name; + ns->spawn.trader = false; + ns->spawn.npc_tint_id = GetNpcTintId(); } void NPC::PetOnSpawn(NewSpawn_Struct* ns) diff --git a/zone/perl_npc.cpp b/zone/perl_npc.cpp index 8498f4a6e..df8b5a924 100644 --- a/zone/perl_npc.cpp +++ b/zone/perl_npc.cpp @@ -885,6 +885,11 @@ Spawn2* Perl_NPC_GetSpawn(NPC* self) return self->GetSpawn(); } +void Perl_NPC_SetNPCTintIndex(NPC* self, uint32 id) +{ + return self->SendAppearancePacket(AppearanceType::NPCTintIndex, id); +} + void perl_register_npc() { perl::interpreter perl(PERL_GET_THX); @@ -1034,6 +1039,7 @@ void perl_register_npc() package.add("SetGold", &Perl_NPC_SetGold); package.add("SetGrid", &Perl_NPC_SetGrid); package.add("SetNPCFactionID", &Perl_NPC_SetNPCFactionID); + package.add("SetNPCTintIndex", &Perl_NPC_SetNPCTintIndex); package.add("SetPetSpellID", &Perl_NPC_SetPetSpellID); package.add("SetPlatinum", &Perl_NPC_SetPlatinum); package.add("SetPrimSkill", &Perl_NPC_SetPrimSkill); diff --git a/zone/zonedb.cpp b/zone/zonedb.cpp index 2dee4d439..3061fc750 100644 --- a/zone/zonedb.cpp +++ b/zone/zonedb.cpp @@ -1732,6 +1732,7 @@ const NPCType *ZoneDatabase::LoadNPCTypesData(uint32 npc_type_id, bool bulk_load t->attack_count = n.attack_count; t->is_parcel_merchant = n.is_parcel_merchant ? true : false; t->greed = n.greed; + t->m_npc_tint_id = n.npc_tint_id; if (!n.special_abilities.empty()) { strn0cpy(t->special_abilities, n.special_abilities.c_str(), 512); diff --git a/zone/zonedump.h b/zone/zonedump.h index 94adecf96..5d29a252b 100644 --- a/zone/zonedump.h +++ b/zone/zonedump.h @@ -157,6 +157,7 @@ struct NPCType bool is_parcel_merchant; uint8 greed; bool multiquest_enabled; + uint32 m_npc_tint_id; }; #pragma pack()