diff --git a/docs/fake/rollnw/__init__.py b/docs/fake/rollnw/__init__.py index 8425cf6ae..2d7610150 100644 --- a/docs/fake/rollnw/__init__.py +++ b/docs/fake/rollnw/__init__.py @@ -1364,6 +1364,12 @@ def model_to_plt_colors(self) -> PltColors: """Converts model colors to PLT colors""" pass + def get_icon_by_part(self, part: ItemModelParts = ItemModelParts.model1, female: bool = False) -> Optional[Image]: + """Generates image of icon by model part, constructing from PLT textures as necessary. + + Note: Python owns the lifetime of the resulting image. Merging icons is also the responsibility of the caller. + """ + @staticmethod def from_dict(value: dict): """Constructs object from python dict. diff --git a/lib/nw/objects/Item.cpp b/lib/nw/objects/Item.cpp index 2f801dcd3..7337f0ebc 100644 --- a/lib/nw/objects/Item.cpp +++ b/lib/nw/objects/Item.cpp @@ -1,6 +1,8 @@ #include "Item.hpp" #include "../formats/Plt.hpp" +#include "../kernel/Resources.hpp" +#include "../kernel/Rules.hpp" #include "../kernel/TwoDACache.hpp" #include "../serialization/Gff.hpp" #include "../serialization/GffBuilder.hpp" @@ -37,6 +39,103 @@ bool Item::instantiate() return instantiated_; } +Image* Item::get_icon_by_part(ItemModelParts::type part, bool female) const +{ + std::string resref; + bool is_plt = false; + ResourceData rdata; + + auto bi = nw::kernel::rules().baseitems.get(baseitem); + if (!bi) { + LOG_F(ERROR, "[item] attempting to use invalid base item: {}", *baseitem); + return nullptr; + } + + if (model_type == ItemModelType::simple && part == ItemModelParts::model1) { + resref = fmt::format("i{}_{:03d}", bi->item_class.view(), model_parts[ItemModelParts::model1]); + } else if (model_type == ItemModelType::layered && part == ItemModelParts::model1) { + // In the layerd case, helmets, etc there is also only one part. + if (baseitem == BaseItem::make(80)) { // Cloak + resref = fmt::format("i{}_m_{:03d}", bi->item_class.view(), model_parts[ItemModelParts::model1]); + } else { // helm + resref = fmt::format("i{}_{:03d}", bi->item_class.view(), model_parts[ItemModelParts::model1]); + } + is_plt = true; + } else if (model_type == ItemModelType::composite) { + // In the composite case, weapons, etc there is also only one part. + if (part == ItemModelParts::model1) { + resref = fmt::format("i{}_b_{:03d}", bi->item_class.view(), model_parts[ItemModelParts::model1]); + } else if (part == ItemModelParts::model2) { + resref = fmt::format("i{}_m_{:03d}", bi->item_class.view(), model_parts[ItemModelParts::model2]); + } else if (part == ItemModelParts::model3) { + resref = fmt::format("i{}_t_{:03d}", bi->item_class.view(), model_parts[ItemModelParts::model3]); + } else { + LOG_F(ERROR, "[item] attempting to use invalid model part: {}", int(part)); + return nullptr; + } + } else if (model_type == ItemModelType::armor) { + // Only certain parts of the armor affect the icon. + is_plt = true; + if (part == ItemModelParts::armor_torso) { + resref = fmt::format("ip{}_chest{:03d}", female ? 'f' : 'm', model_parts[part]); + } else if (part == ItemModelParts::armor_robe) { + resref = fmt::format("ip{}_robe{:03d}", female ? 'f' : 'm', model_parts[part]); + } else if (part == ItemModelParts::armor_belt) { + resref = fmt::format("ip{}_belt{:03d}", female ? 'f' : 'm', model_parts[part]); + } else if (part == ItemModelParts::armor_pelvis) { + resref = fmt::format("ip{}_pelvis{:03d}", female ? 'f' : 'm', model_parts[part]); + } else if (part == ItemModelParts::armor_lshoul) { + resref = fmt::format("ip{}_shol{:03d}", female ? 'f' : 'm', model_parts[part]); + } else if (part == ItemModelParts::armor_rshoul) { + resref = fmt::format("ip{}_shor{:03d}", female ? 'f' : 'm', model_parts[part]); + } else { + LOG_F(ERROR, "[item] attempting to use unnecessary model part: {}", int(part)); + return nullptr; + } + } + + if (!is_plt) { + rdata = nw::kernel::resman().demand_in_order(Resref(resref), {ResourceType::dds, ResourceType::tga}); + if (rdata.bytes.size() == 0) { + rdata = nw::kernel::resman().demand_in_order(bi->default_icon, {ResourceType::dds, ResourceType::tga}); + } + if (rdata.bytes.size() == 0) { + LOG_F(ERROR, "[item] failed to load icon or default icon for base type", *baseitem); + return nullptr; + } + auto img = new Image(std::move(rdata)); + if (!img->valid()) { + delete img; + return nullptr; + } + return img; + } else { + rdata = nw::kernel::resman().demand({Resref(resref), ResourceType::plt}); + Plt plt(std::move(rdata)); + if (!plt.valid()) { + rdata = nw::kernel::resman().demand_in_order(bi->default_icon, {ResourceType::dds, ResourceType::tga}); + if (rdata.bytes.size() == 0) { + LOG_F(ERROR, "[item] failed to load icon or default icon for base type", *baseitem); + return nullptr; + } + auto img = new Image(std::move(rdata)); + if (!img->valid()) { + delete img; + return nullptr; + } + } else { + auto img = new Image(plt, model_to_plt_colors()); + if (!img->valid()) { + delete img; + return nullptr; + } + return img; + } + } + + return nullptr; +} + PltColors Item::model_to_plt_colors() const noexcept { PltColors result{0}; diff --git a/lib/nw/objects/Item.hpp b/lib/nw/objects/Item.hpp index 2ca765a38..b92dddd29 100644 --- a/lib/nw/objects/Item.hpp +++ b/lib/nw/objects/Item.hpp @@ -12,6 +12,7 @@ namespace nw { +struct Image; struct PltColors; struct Item : public ObjectBase { @@ -32,6 +33,10 @@ struct Item : public ObjectBase { static bool serialize(const Item* obj, nlohmann::json& archive, SerializationProfile profile); static std::string get_name_from_file(const std::filesystem::path& path); + /// Gets image by item model part. + /// @note Caller takes ownership of the resulting image. Merging icons is also the responsibility of the caller. + Image* get_icon_by_part(ItemModelParts::type part = ItemModelParts::model1, bool female = false) const; + /// Converts model colors to Plt colors PltColors model_to_plt_colors() const noexcept; diff --git a/rollnw-py/wrapper_objects.cpp b/rollnw-py/wrapper_objects.cpp index 552f20e95..7fc72c596 100644 --- a/rollnw-py/wrapper_objects.cpp +++ b/rollnw-py/wrapper_objects.cpp @@ -463,6 +463,7 @@ void init_objects_item(py::module& nw) .def("to_dict", &to_json_helper) .def("handle", &nw::Item::handle) .def("model_to_plt_colors", &nw::Item::model_to_plt_colors) + .def("get_icon_by_part", &nw::Item::get_icon_by_part) .def_readonly_static("json_archive_version", &nw::Item::json_archive_version) .def_readonly_static("object_type", &nw::Item::object_type) diff --git a/rollnw-stubs/__init__.pyi b/rollnw-stubs/__init__.pyi index 4f0b18d53..794456eca 100644 --- a/rollnw-stubs/__init__.pyi +++ b/rollnw-stubs/__init__.pyi @@ -800,6 +800,7 @@ class Item(ObjectBase): def inventory(self) -> Inventory: ... def handle(self) -> ObjectHandle: ... + def get_icon_by_part(self, part:ItemModelParts = ItemModelParts.model1, female:bool = False) ->Image: ... def model_to_plt_colors(self) -> PltColors: ... diff --git a/tests/objects_item.cpp b/tests/objects_item.cpp index 88d30c8ad..3201b5a6c 100644 --- a/tests/objects_item.cpp +++ b/tests/objects_item.cpp @@ -179,3 +179,77 @@ TEST(Item, GffRoundTrip) EXPECT_EQ(oa.header.list_idx_offset, g.head_->list_idx_offset); EXPECT_EQ(oa.header.list_idx_count, g.head_->list_idx_count); } + +TEST(Item, Icon) +{ + // Can't run these tests against the docker/server distro since it strips models, + // textures, etc. +#if 0 + auto mod = nw::kernel::load_module("test_data/user/modules/DockerDemo.mod"); + EXPECT_TRUE(mod); + + // Compound + auto item1 = nw::kernel::objects().load("nw_wswbs001"sv); + EXPECT_TRUE(item1); + auto icon1_1 = item1->get_icon_by_part(nw::ItemModelParts::model1); + EXPECT_TRUE(icon1_1); + EXPECT_TRUE(icon1_1->valid()); + EXPECT_TRUE(icon1_1->write_to("tmp/wswbs001_b.png")); + delete icon1_1; + + auto icon1_2 = item1->get_icon_by_part(nw::ItemModelParts::model2); + EXPECT_TRUE(icon1_2); + EXPECT_TRUE(icon1_2->valid()); + EXPECT_TRUE(icon1_2->write_to("tmp/wswbs001_m.png")); + delete icon1_2; + + auto icon1_3 = item1->get_icon_by_part(nw::ItemModelParts::model3); + EXPECT_TRUE(icon1_3); + EXPECT_TRUE(icon1_3->valid()); + EXPECT_TRUE(icon1_3->write_to("tmp/wswbs001_t.png")); + delete icon1_3; + + // Simple + auto item2 = nw::kernel::objects().load("x2_smchaosshield"sv); + EXPECT_TRUE(item2); + auto icon2_1 = item2->get_icon_by_part(); + EXPECT_TRUE(icon2_1); + EXPECT_TRUE(icon2_1->valid()); + EXPECT_TRUE(icon2_1->write_to("tmp/x2_smchaosshield.png")); + delete icon2_1; + + // Layered + auto item3 = nw::kernel::objects().load("x2_helm_002"sv); + EXPECT_TRUE(item3); + auto icon3_1 = item3->get_icon_by_part(); + EXPECT_TRUE(icon3_1); + EXPECT_TRUE(icon3_1->valid()); + EXPECT_TRUE(icon3_1->write_to("tmp/x2_helm_002.png")); + delete icon3_1; + + auto item4 = nw::kernel::objects().load("x2_it_drowcl001"sv); + EXPECT_TRUE(item4); + auto icon4_1 = item4->get_icon_by_part(); + EXPECT_TRUE(icon4_1); + EXPECT_TRUE(icon4_1->valid()); + EXPECT_TRUE(icon4_1->write_to("tmp/x2_it_drowcl001.png")); + delete icon4_1; + + // Armor + auto item5 = nw::kernel::objects().load("x2_it_adaplate"sv); + EXPECT_TRUE(item5); + auto icon5_1 = item5->get_icon_by_part(nw::ItemModelParts::armor_torso); + EXPECT_TRUE(icon5_1); + EXPECT_TRUE(icon5_1->valid()); + EXPECT_TRUE(icon5_1->write_to("tmp/x2_it_adaplate.png")); + delete icon5_1; + + auto icon5_2 = item5->get_icon_by_part(nw::ItemModelParts::armor_torso, true); + EXPECT_TRUE(icon5_2); + EXPECT_TRUE(icon5_2->valid()); + EXPECT_TRUE(icon5_2->write_to("tmp/x2_it_adaplatef.png")); + delete icon5_2; + + nw::kernel::unload_module(); +#endif +}