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..5a811fb43 100644 --- a/common/repositories/base/base_npc_types_repository.h +++ b/common/repositories/base/base_npc_types_repository.h @@ -66,8 +66,8 @@ public: uint8_t armortint_red; uint8_t armortint_green; uint8_t armortint_blue; - uint32_t d_melee_texture1; - uint32_t d_melee_texture2; + int32_t d_melee_texture1; + int32_t d_melee_texture2; std::string ammo_idfile; uint8_t prim_melee_type; uint8_t sec_melee_type; @@ -123,7 +123,7 @@ public: int8_t legtexture; int8_t feettexture; int8_t light; - float walkspeed; + int8_t walkspeed; int32_t peqid; int8_t unique_; int8_t fixed; @@ -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; } @@ -678,8 +682,8 @@ public: e.armortint_red = row[44] ? static_cast(strtoul(row[44], nullptr, 10)) : 0; e.armortint_green = row[45] ? static_cast(strtoul(row[45], nullptr, 10)) : 0; e.armortint_blue = row[46] ? static_cast(strtoul(row[46], nullptr, 10)) : 0; - e.d_melee_texture1 = row[47] ? static_cast(strtoul(row[47], nullptr, 10)) : 0; - e.d_melee_texture2 = row[48] ? static_cast(strtoul(row[48], nullptr, 10)) : 0; + e.d_melee_texture1 = row[47] ? static_cast(atoi(row[47])) : 0; + e.d_melee_texture2 = row[48] ? static_cast(atoi(row[48])) : 0; e.ammo_idfile = row[49] ? row[49] : "IT10"; e.prim_melee_type = row[50] ? static_cast(strtoul(row[50], nullptr, 10)) : 28; e.sec_melee_type = row[51] ? static_cast(strtoul(row[51], nullptr, 10)) : 28; @@ -735,7 +739,7 @@ public: e.legtexture = row[101] ? static_cast(atoi(row[101])) : 0; e.feettexture = row[102] ? static_cast(atoi(row[102])) : 0; e.light = row[103] ? static_cast(atoi(row[103])) : 0; - e.walkspeed = row[104] ? strtof(row[104], nullptr) : 0; + e.walkspeed = row[104] ? static_cast(atoi(row[104])) : 0; e.peqid = row[105] ? static_cast(atoi(row[105])) : 0; e.unique_ = row[106] ? static_cast(atoi(row[106])) : 0; e.fixed = row[107] ? static_cast(atoi(row[107])) : 0; @@ -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) + ")"); } @@ -1313,8 +1321,8 @@ public: e.armortint_red = row[44] ? static_cast(strtoul(row[44], nullptr, 10)) : 0; e.armortint_green = row[45] ? static_cast(strtoul(row[45], nullptr, 10)) : 0; e.armortint_blue = row[46] ? static_cast(strtoul(row[46], nullptr, 10)) : 0; - e.d_melee_texture1 = row[47] ? static_cast(strtoul(row[47], nullptr, 10)) : 0; - e.d_melee_texture2 = row[48] ? static_cast(strtoul(row[48], nullptr, 10)) : 0; + e.d_melee_texture1 = row[47] ? static_cast(atoi(row[47])) : 0; + e.d_melee_texture2 = row[48] ? static_cast(atoi(row[48])) : 0; e.ammo_idfile = row[49] ? row[49] : "IT10"; e.prim_melee_type = row[50] ? static_cast(strtoul(row[50], nullptr, 10)) : 28; e.sec_melee_type = row[51] ? static_cast(strtoul(row[51], nullptr, 10)) : 28; @@ -1370,7 +1378,7 @@ public: e.legtexture = row[101] ? static_cast(atoi(row[101])) : 0; e.feettexture = row[102] ? static_cast(atoi(row[102])) : 0; e.light = row[103] ? static_cast(atoi(row[103])) : 0; - e.walkspeed = row[104] ? strtof(row[104], nullptr) : 0; + e.walkspeed = row[104] ? static_cast(atoi(row[104])) : 0; e.peqid = row[105] ? static_cast(atoi(row[105])) : 0; e.unique_ = row[106] ? static_cast(atoi(row[106])) : 0; e.fixed = row[107] ? static_cast(atoi(row[107])) : 0; @@ -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); } @@ -1467,8 +1476,8 @@ public: e.armortint_red = row[44] ? static_cast(strtoul(row[44], nullptr, 10)) : 0; e.armortint_green = row[45] ? static_cast(strtoul(row[45], nullptr, 10)) : 0; e.armortint_blue = row[46] ? static_cast(strtoul(row[46], nullptr, 10)) : 0; - e.d_melee_texture1 = row[47] ? static_cast(strtoul(row[47], nullptr, 10)) : 0; - e.d_melee_texture2 = row[48] ? static_cast(strtoul(row[48], nullptr, 10)) : 0; + e.d_melee_texture1 = row[47] ? static_cast(atoi(row[47])) : 0; + e.d_melee_texture2 = row[48] ? static_cast(atoi(row[48])) : 0; e.ammo_idfile = row[49] ? row[49] : "IT10"; e.prim_melee_type = row[50] ? static_cast(strtoul(row[50], nullptr, 10)) : 28; e.sec_melee_type = row[51] ? static_cast(strtoul(row[51], nullptr, 10)) : 28; @@ -1524,7 +1533,7 @@ public: e.legtexture = row[101] ? static_cast(atoi(row[101])) : 0; e.feettexture = row[102] ? static_cast(atoi(row[102])) : 0; e.light = row[103] ? static_cast(atoi(row[103])) : 0; - e.walkspeed = row[104] ? strtof(row[104], nullptr) : 0; + e.walkspeed = row[104] ? static_cast(atoi(row[104])) : 0; e.peqid = row[105] ? static_cast(atoi(row[105])) : 0; e.unique_ = row[106] ? static_cast(atoi(row[106])) : 0; e.fixed = row[107] ? static_cast(atoi(row[107])) : 0; @@ -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/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/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/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()