Skip to content

Commit

Permalink
[objects] add: Item::get_icon_by_part which generates an icon of a pa…
Browse files Browse the repository at this point in the history
…rticular model part of any item.

Note that merging in the case of armor and compound model types is the responsibility of the caller.
  • Loading branch information
jd28 committed Jul 10, 2024
1 parent d6f1c8b commit dc72b0b
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/fake/rollnw/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
99 changes: 99 additions & 0 deletions lib/nw/objects/Item.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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};
Expand Down
5 changes: 5 additions & 0 deletions lib/nw/objects/Item.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace nw {

struct Image;
struct PltColors;

struct Item : public ObjectBase {
Expand All @@ -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;

Expand Down
1 change: 1 addition & 0 deletions rollnw-py/wrapper_objects.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ void init_objects_item(py::module& nw)
.def("to_dict", &to_json_helper<nw::Item>)
.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)
Expand Down
1 change: 1 addition & 0 deletions rollnw-stubs/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...


Expand Down
74 changes: 74 additions & 0 deletions tests/objects_item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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::Item>("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<nw::Item>("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<nw::Item>("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<nw::Item>("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<nw::Item>("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
}

0 comments on commit dc72b0b

Please sign in to comment.