From 176e674a51bb27f3c239dd217214dca0a2f2a1d1 Mon Sep 17 00:00:00 2001 From: techno-sam <77073745+techno-sam@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:21:00 -0800 Subject: [PATCH] Add wear bar color API (#13328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Muhammad Rifqi Priyo Susanto Co-authored-by: Lars Müller <34514239+appgurueu@users.noreply.github.com> Co-authored-by: grorp --- doc/lua_api.md | 59 ++++++++++ games/devtest/mods/basetools/init.lua | 137 +++++++++++++++++++--- games/devtest/mods/util_commands/init.lua | 23 ++++ src/client/hud.cpp | 23 ++-- src/inventory.h | 9 ++ src/itemdef.cpp | 14 +++ src/itemdef.h | 3 + src/itemstackmetadata.cpp | 29 +++++ src/itemstackmetadata.h | 17 ++- src/script/common/c_content.cpp | 78 ++++++++++++ src/script/common/c_content.h | 3 + src/script/lua_api/l_item.cpp | 17 +++ src/script/lua_api/l_item.h | 5 + src/script/lua_api/l_itemstackmeta.cpp | 17 +++ src/script/lua_api/l_itemstackmeta.h | 11 ++ src/tool.cpp | 124 ++++++++++++++++++++ src/tool.h | 40 ++++++- src/util/string.cpp | 14 +++ src/util/string.h | 1 + 19 files changed, 598 insertions(+), 26 deletions(-) diff --git a/doc/lua_api.md b/doc/lua_api.md index a38b5be0d014..b4ce001ddc75 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -7273,6 +7273,8 @@ an itemstring, a table or `nil`. the item breaks after `max_uses` times * Valid `max_uses` range is [0,65536] * Does nothing if item is not a tool or if `max_uses` is 0 +* `get_wear_bar_params()`: returns the wear bar parameters of the item, + or nil if none are defined for this item type or in the stack's meta * `add_item(item)`: returns leftover `ItemStack` * Put some item or stack onto this stack * `item_fits(item)`: returns `true` if item or stack can be fully added to @@ -7314,6 +7316,10 @@ Can be obtained via `item:get_meta()`. * Overrides the item's tool capabilities * A nil value will clear the override data and restore the original behavior. +* `set_wear_bar_params([wear_bar_params])` + * Overrides the item's wear bar parameters (see "Wear Bar Color" section) + * A nil value will clear the override data and restore the original + behavior. `MetaDataRef` ------------- @@ -8815,6 +8821,19 @@ Used by `minetest.register_node`, `minetest.register_craftitem`, and -- fallback behavior. }, + -- Set wear bar color of the tool by setting color stops and blend mode + -- See "Wear Bar Color" section for further explanation including an example + wear_color = { + -- interpolation mode: 'constant' or 'linear' + -- (nil defaults to 'constant') + blend = "linear", + color_stops = { + [0.0] = "#ff0000", + [0.5] = "#ffff00", + [1.0] = "#00ff00", + } + }, + node_placement_prediction = nil, -- If nil and item is node, prediction is made automatically. -- If nil and item is not a node, no prediction is made. @@ -9382,6 +9401,46 @@ Used by `minetest.register_node`. } ``` +Wear Bar Color +-------------- + +'Wear Bar' is a property of items that defines the coloring +of the bar that appears under damaged tools. +If it is absent, the default behavior of green-yellow-red is +used. + +### Wear bar colors definition + +#### Syntax + +```lua +{ + -- 'constant' or 'linear' + -- (nil defaults to 'constant') + blend = "linear", + color_stops = { + [0.0] = "#ff0000", + [0.5] = "slateblue", + [1.0] = {r=0, g=255, b=0, a=150}, + } +} +``` + +#### Blend mode `blend` + +* `linear`: blends smoothly between each defined color point. +* `constant`: each color starts at its defined point, and continues up to the next point + +#### Color stops `color_stops` + +Specified as `ColorSpec` color values assigned to `float` durability keys. + +"Durability" is defined as `1 - (wear / 65535)`. + +#### Shortcut usage + +Wear bar color can also be specified as a single `ColorSpec` instead of a table. + Crafting recipes ---------------- diff --git a/games/devtest/mods/basetools/init.lua b/games/devtest/mods/basetools/init.lua index c0bd1d00bca4..aa91d5e92478 100644 --- a/games/devtest/mods/basetools/init.lua +++ b/games/devtest/mods/basetools/init.lua @@ -420,36 +420,141 @@ minetest.register_tool("basetools:dagger_steel", { } }) --- Test tool uses and punch_attack_uses -local uses = { 1, 2, 3, 5, 10, 50, 100, 1000, 10000, 65535 } -for i=1, #uses do - local u = uses[i] - local ustring - if i == 1 then - ustring = u.."-Use" - else - ustring = u.."-Uses" - end - local color = string.format("#FF00%02X", math.floor(((i-1)/#uses) * 255)) - minetest.register_tool("basetools:pick_uses_"..string.format("%05d", u), { +-- Test tool uses, punch_attack_uses, and wear bar coloring +local tool_params = { + {uses = 1}, + {uses = 2}, + {uses = 3}, + { + uses = 5, + wear_color = "#5865f2", + wear_description = "Solid color: #5865f2", + }, + { + uses = 10, + wear_color = "slateblue", + wear_description = "Solid color: slateblue", + }, + { + uses = 50, + wear_color = { + color_stops = { + [0] = "red", + [0.5] = "yellow", + [1.0] = "blue" + }, + blend = "linear" + }, + wear_description = "Ranges from blue to yellow to red", + }, + { + uses = 100, + wear_color = { + color_stops = { + [0] = "#ffff00", + [0.2] = "#ff00ff", + [0.3] = "#ffff00", + [0.45] = "#c0ffee", + [0.6] = {r=255, g=255, b=0, a=100}, -- continues until the end + }, + blend = "constant" + }, + wear_description = "Misc. colors, constant interpolation", + }, + {uses = 1e3}, + {uses = 1e4}, + {uses = 65535}, +} + +for i, params in ipairs(tool_params) do + local uses = params.uses + local ustring = uses.."-Use"..(uses == 1 and "" or "s") + local color = string.format("#FF00%02X", math.floor(((i-1)/#tool_params) * 255)) + minetest.register_tool("basetools:pick_uses_"..string.format("%05d", uses), { description = ustring.." Pickaxe".."\n".. - "Digs cracky=3", + "Digs cracky=3".. + (params.wear_description and "\n".."Wear bar: " .. params.wear_description or ""), inventory_image = "basetools_usespick.png^[colorize:"..color..":127", tool_capabilities = { max_drop_level=0, groupcaps={ - cracky={times={[3]=0.1, [2]=0.2, [1]=0.3}, uses=u, maxlevel=0} + cracky={times={[3]=0.1, [2]=0.2, [1]=0.3}, uses=uses, maxlevel=0} }, }, + wear_color = params.wear_color }) - minetest.register_tool("basetools:sword_uses_"..string.format("%05d", u), { + minetest.register_tool("basetools:sword_uses_"..string.format("%05d", uses), { description = ustring.." Sword".."\n".. "Damage: fleshy=1", inventory_image = "basetools_usessword.png^[colorize:"..color..":127", tool_capabilities = { damage_groups = {fleshy=1}, - punch_attack_uses = u, + punch_attack_uses = uses, }, }) end + +minetest.register_chatcommand("wear_color", { + params = "[idx]", + description = "Set wear bar color override", + func = function(player_name, param) + local player = minetest.get_player_by_name(player_name) + if not player then return end + + local wear_color = nil + local wear_desc = "Reset override" + + if param ~= "" then + local params = tool_params[tonumber(param)] + if not params then + return false, "idx out of bounds" + end + wear_color = params.wear_color + wear_desc = "Set override: "..(params.wear_description or "Default behavior") + end + local tool = player:get_wielded_item() + if tool:get_count() == 0 then + return false, "Tool not found" + end + tool:get_meta():set_wear_bar_params(wear_color) + player:set_wielded_item(tool) + return true, wear_desc + end +}) + +-- Punch handler to set random color & wear +local wear_on_use = function(itemstack, user, pointed_thing) + local meta = itemstack:get_meta() + local color = math.random(0, 0xFFFFFF) + local colorstr = string.format("#%06x", color) + meta:set_wear_bar_params(colorstr) + minetest.log("action", "[basetool] Wear bar color of "..itemstack:get_name().." changed to "..colorstr) + itemstack:set_wear(math.random(0, 65535)) + return itemstack +end + +-- Place handler to clear item metadata color +local wear_on_place = function(itemstack, user, pointed_thing) + local meta = itemstack:get_meta() + meta:set_wear_bar_params(nil) + return itemstack +end + +minetest.register_tool("basetools:random_wear_bar", { + description = "Wear Bar Color Test\n" .. + "Punch: Set random color & wear\n" .. + "Place: Clear color", + -- Base texture: A grayscale square (can be colorized) + inventory_image = "basetools_usespick.png^[colorize:#FFFFFF:127", + tool_capabilities = { + max_drop_level=0, + groupcaps={ + cracky={times={[3]=0.1, [2]=0.2, [1]=0.3}, uses=1000, maxlevel=0} + }, + }, + + on_use = wear_on_use, + on_place = wear_on_place, + on_secondary_use = wear_on_place, +}) diff --git a/games/devtest/mods/util_commands/init.lua b/games/devtest/mods/util_commands/init.lua index 48cd47f10732..6285d4754f66 100644 --- a/games/devtest/mods/util_commands/init.lua +++ b/games/devtest/mods/util_commands/init.lua @@ -210,6 +210,29 @@ minetest.register_chatcommand("dump_item", { end, }) +minetest.register_chatcommand("dump_itemdef", { + params = "", + description = "Prints a dump of the wielded item's definition in table form", + func = function(name, param) + local player = minetest.get_player_by_name(name) + local str = dump(player:get_wielded_item():get_definition()) + print(str) + return true, str + end, +}) + +minetest.register_chatcommand("dump_wear_bar", { + params = "", + description = "Prints a dump of the wielded item's wear bar parameters in table form", + func = function(name, param) + local player = minetest.get_player_by_name(name) + local item = player:get_wielded_item() + local str = dump(item:get_wear_bar_params()) + print(str) + return true, str + end, +}) + core.register_chatcommand("set_saturation", { params = "", description = "Set the saturation for current player.", diff --git a/src/client/hud.cpp b/src/client/hud.cpp index 1618b757f7ac..1201b95b920c 100644 --- a/src/client/hud.cpp +++ b/src/client/hud.cpp @@ -1179,17 +1179,26 @@ void drawItemStack( (1 - wear) * progressrect.LowerRightCorner.X; // Compute progressbar color + // default scheme: // wear = 0.0: green // wear = 0.5: yellow // wear = 1.0: red - video::SColor color(255, 255, 255, 255); - int wear_i = MYMIN(std::floor(wear * 600), 511); - wear_i = MYMIN(wear_i + 10, 511); - if (wear_i <= 255) - color.set(255, wear_i, 255, 0); - else - color.set(255, 255, 511 - wear_i, 0); + video::SColor color; + auto barParams = item.getWearBarParams(client->idef()); + if (barParams.has_value()) { + f32 durabilityPercent = 1.0 - wear; + color = barParams->getWearBarColor(durabilityPercent); + } else { + color = video::SColor(255, 255, 255, 255); + int wear_i = MYMIN(std::floor(wear * 600), 511); + wear_i = MYMIN(wear_i + 10, 511); + + if (wear_i <= 255) + color.set(255, wear_i, 255, 0); + else + color.set(255, 255, 511 - wear_i, 0); + } core::rect progressrect2 = progressrect; progressrect2.LowerRightCorner.X = progressmid; diff --git a/src/inventory.h b/src/inventory.h index 64ca857fcac7..e92893a8ea82 100644 --- a/src/inventory.h +++ b/src/inventory.h @@ -131,6 +131,15 @@ struct ItemStack return metadata.getToolCapabilities(*item_cap); // Check for override } + const std::optional &getWearBarParams( + const IItemDefManager *itemdef) const + { + auto &meta_override = metadata.getWearBarParamOverride(); + if (meta_override.has_value()) + return meta_override; + return itemdef->get(name).wear_bar_params; + } + // Wear out (only tools) // Returns true if the item is (was) a tool bool addWear(s32 amount, const IItemDefManager *itemdef) diff --git a/src/itemdef.cpp b/src/itemdef.cpp index ee3e3829d90d..5c63270c005a 100644 --- a/src/itemdef.cpp +++ b/src/itemdef.cpp @@ -125,6 +125,7 @@ ItemDefinition& ItemDefinition::operator=(const ItemDefinition &def) pointabilities = def.pointabilities; if (def.tool_capabilities) tool_capabilities = new ToolCapabilities(*def.tool_capabilities); + wear_bar_params = def.wear_bar_params; groups = def.groups; node_placement_prediction = def.node_placement_prediction; place_param2 = def.place_param2; @@ -149,6 +150,7 @@ void ItemDefinition::resetInitial() { // Initialize pointers to NULL so reset() does not delete undefined pointers tool_capabilities = NULL; + wear_bar_params = std::nullopt; reset(); } @@ -171,6 +173,7 @@ void ItemDefinition::reset() pointabilities = std::nullopt; delete tool_capabilities; tool_capabilities = NULL; + wear_bar_params.reset(); groups.clear(); sound_place = SoundSpec(); sound_place_failed = SoundSpec(); @@ -251,6 +254,13 @@ void ItemDefinition::serialize(std::ostream &os, u16 protocol_version) const pointabilities_s = tmp_os.str(); } os << serializeString16(pointabilities_s); + + if (wear_bar_params.has_value()) { + writeU8(os, 1); + wear_bar_params->serialize(os); + } else { + writeU8(os, 0); + } } void ItemDefinition::deSerialize(std::istream &is, u16 protocol_version) @@ -333,6 +343,10 @@ void ItemDefinition::deSerialize(std::istream &is, u16 protocol_version) pointabilities = std::make_optional(); pointabilities->deSerialize(tmp_is); } + + if (readU8(is)) { + wear_bar_params = WearBarParams::deserialize(is); + } } catch(SerializationError &e) {}; } diff --git a/src/itemdef.h b/src/itemdef.h index 192e90095255..f8cb1b613649 100644 --- a/src/itemdef.h +++ b/src/itemdef.h @@ -28,6 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "itemgroup.h" #include "sound.h" #include "texture_override.h" // TextureOverride +#include "tool.h" #include "util/pointabilities.h" class IGameDef; class Client; @@ -103,6 +104,8 @@ struct ItemDefinition // They may be NULL. If non-NULL, deleted by destructor ToolCapabilities *tool_capabilities; + std::optional wear_bar_params; + ItemGroupList groups; SoundSpec sound_place; SoundSpec sound_place_failed; diff --git a/src/itemstackmetadata.cpp b/src/itemstackmetadata.cpp index c8ce2449f622..bf4a7b2ac7ed 100644 --- a/src/itemstackmetadata.cpp +++ b/src/itemstackmetadata.cpp @@ -21,7 +21,9 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "itemstackmetadata.h" #include "util/serialize.h" #include "util/strfnd.h" + #include +#include #define DESERIALIZE_START '\x01' #define DESERIALIZE_KV_DELIM '\x02' @@ -31,11 +33,13 @@ with this program; if not, write to the Free Software Foundation, Inc., #define DESERIALIZE_PAIR_DELIM_STR "\x03" #define TOOLCAP_KEY "tool_capabilities" +#define WEAR_BAR_KEY "wear_color" void ItemStackMetadata::clear() { SimpleMetadata::clear(); updateToolCapabilities(); + updateWearBarParams(); } static void sanitize_string(std::string &str) @@ -55,6 +59,8 @@ bool ItemStackMetadata::setString(const std::string &name, const std::string &va bool result = SimpleMetadata::setString(clean_name, clean_var); if (clean_name == TOOLCAP_KEY) updateToolCapabilities(); + else if (clean_name == WEAR_BAR_KEY) + updateWearBarParams(); return result; } @@ -91,6 +97,7 @@ void ItemStackMetadata::deSerialize(std::istream &is) } } updateToolCapabilities(); + updateWearBarParams(); } void ItemStackMetadata::updateToolCapabilities() @@ -116,3 +123,25 @@ void ItemStackMetadata::clearToolCapabilities() { setString(TOOLCAP_KEY, ""); } + +void ItemStackMetadata::updateWearBarParams() +{ + if (contains(WEAR_BAR_KEY)) { + std::istringstream is(getString(WEAR_BAR_KEY)); + wear_bar_override = WearBarParams::deserializeJson(is); + } else { + wear_bar_override.reset(); + } +} + +void ItemStackMetadata::setWearBarParams(const WearBarParams ¶ms) +{ + std::ostringstream os; + params.serializeJson(os); + setString(WEAR_BAR_KEY, os.str()); +} + +void ItemStackMetadata::clearWearBarParams() +{ + setString(WEAR_BAR_KEY, ""); +} diff --git a/src/itemstackmetadata.h b/src/itemstackmetadata.h index 48a029c51f94..e0615916ed3f 100644 --- a/src/itemstackmetadata.h +++ b/src/itemstackmetadata.h @@ -22,13 +22,17 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "metadata.h" #include "tool.h" +#include + class Inventory; class IItemDefManager; class ItemStackMetadata : public SimpleMetadata { public: - ItemStackMetadata() : toolcaps_overridden(false) {} + ItemStackMetadata(): + toolcaps_overridden(false) + {} // Overrides void clear() override; @@ -46,9 +50,20 @@ class ItemStackMetadata : public SimpleMetadata void setToolCapabilities(const ToolCapabilities &caps); void clearToolCapabilities(); + const std::optional &getWearBarParamOverride() const + { + return wear_bar_override; + } + + + void setWearBarParams(const WearBarParams ¶ms); + void clearWearBarParams(); + private: void updateToolCapabilities(); + void updateWearBarParams(); bool toolcaps_overridden; ToolCapabilities toolcaps_override; + std::optional wear_bar_override; }; diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp index 5259d3f38437..aab91f35c1dd 100644 --- a/src/script/common/c_content.cpp +++ b/src/script/common/c_content.cpp @@ -35,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "server/player_sao.h" #include "util/pointedthing.h" #include "debug.h" // For FATAL_ERROR +#include #include struct EnumString es_TileAnimationType[] = @@ -94,6 +95,15 @@ void read_item_definition(lua_State* L, int index, def.tool_capabilities = new ToolCapabilities( read_tool_capabilities(L, -1)); } + lua_getfield(L, index, "wear_color"); + if (lua_istable(L, -1)) { + def.wear_bar_params = read_wear_bar_params(L, -1); + } else if (lua_isstring(L, -1)) { + video::SColor color; + read_color(L, -1, &color); + def.wear_bar_params = WearBarParams({{0.0, color}}, + WearBarParams::BLEND_MODE_CONSTANT); + } // If name is "" (hand), ensure there are ToolCapabilities // because it will be looked up there whenever any other item has @@ -213,6 +223,10 @@ void push_item_definition_full(lua_State *L, const ItemDefinition &i) push_tool_capabilities(L, *i.tool_capabilities); lua_setfield(L, -2, "tool_capabilities"); } + if (i.wear_bar_params.has_value()) { + push_wear_bar_params(L, *i.wear_bar_params); + lua_setfield(L, -2, "wear_color"); + } push_groups(L, i.groups); lua_setfield(L, -2, "groups"); push_simplesoundspec(L, i.sound_place); @@ -1454,6 +1468,22 @@ void push_tool_capabilities(lua_State *L, lua_setfield(L, -2, "damage_groups"); } +/******************************************************************************/ +void push_wear_bar_params(lua_State *L, + const WearBarParams ¶ms) +{ + lua_newtable(L); + setstringfield(L, -1, "blend", WearBarParams::es_BlendMode[params.blend].str); + + lua_newtable(L); + for (const std::pair item: params.colorStops) { + lua_pushnumber(L, item.first); // key + push_ARGB8(L, item.second); + lua_rawset(L, -3); + } + lua_setfield(L, -2, "color_stops"); +} + /******************************************************************************/ void push_inventory_list(lua_State *L, const InventoryList &invlist) { @@ -1732,6 +1762,54 @@ void push_pointabilities(lua_State *L, const Pointabilities &pointabilities) } } +/******************************************************************************/ +WearBarParams read_wear_bar_params( + lua_State *L, int stack_idx) +{ + if (lua_isstring(L, stack_idx)) { + video::SColor color; + read_color(L, stack_idx, &color); + return WearBarParams(color); + } + + if (!lua_istable(L, stack_idx)) + throw LuaError("Expected wear bar color table or colorstring"); + + lua_getfield(L, stack_idx, "color_stops"); + if (!check_field_or_nil(L, -1, LUA_TTABLE, "color_stops")) + throw LuaError("color_stops must be a table"); + + std::map colorStops; + // color stops table is on the stack + int table_values = lua_gettop(L); + lua_pushnil(L); + while (lua_next(L, table_values) != 0) { + // key at index -2 and value at index -1 within table_values + f32 point = luaL_checknumber(L, -2); + if (point < 0 || point > 1) + throw LuaError("Wear bar color stop key out of range"); + video::SColor color; + read_color(L, -1, &color); + colorStops.emplace(point, color); + + // removes value, keeps key for next iteration + lua_pop(L, 1); + } + lua_pop(L, 1); // pop color stops table + + auto blend = WearBarParams::BLEND_MODE_CONSTANT; + lua_getfield(L, stack_idx, "blend"); + if (check_field_or_nil(L, -1, LUA_TSTRING, "blend")) { + int blendInt; + if (!string_to_enum(WearBarParams::es_BlendMode, blendInt, std::string(lua_tostring(L, -1)))) + throw LuaError("Invalid wear bar color blend mode"); + blend = static_cast(blendInt); + } + lua_pop(L, 1); + + return WearBarParams(colorStops, blend); +} + /******************************************************************************/ void push_dig_params(lua_State *L,const DigParams ¶ms) { diff --git a/src/script/common/c_content.h b/src/script/common/c_content.h index 6f54b7b23a0d..7aa7dca8d808 100644 --- a/src/script/common/c_content.h +++ b/src/script/common/c_content.h @@ -116,6 +116,9 @@ void push_pointabilities (lua_State *L, const Pointabilities ToolCapabilities read_tool_capabilities (lua_State *L, int table); void push_tool_capabilities (lua_State *L, const ToolCapabilities &prop); +WearBarParams read_wear_bar_params (lua_State *L, int table); +void push_wear_bar_params (lua_State *L, + const WearBarParams &prop); void read_item_definition (lua_State *L, int index, const ItemDefinition &default_def, ItemDefinition &def); diff --git a/src/script/lua_api/l_item.cpp b/src/script/lua_api/l_item.cpp index c68046b5d7de..875e5dedd45e 100644 --- a/src/script/lua_api/l_item.cpp +++ b/src/script/lua_api/l_item.cpp @@ -376,6 +376,22 @@ int LuaItemStack::l_add_wear_by_uses(lua_State *L) return 1; } +// get_wear_bar_params(self) -> table +// Returns the effective wear bar parameters. +// Returns nil if this item has none associated. +int LuaItemStack::l_get_wear_bar_params(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + LuaItemStack *o = checkObject(L, 1); + ItemStack &item = o->m_stack; + auto params = item.getWearBarParams(getGameDef(L)->idef()); + if (params.has_value()) { + push_wear_bar_params(L, *params); + return 1; + } + return 0; +} + // add_item(self, itemstack or itemstring or table or nil) -> itemstack // Returns leftover item stack int LuaItemStack::l_add_item(lua_State *L) @@ -551,6 +567,7 @@ const luaL_Reg LuaItemStack::methods[] = { luamethod(LuaItemStack, get_tool_capabilities), luamethod(LuaItemStack, add_wear), luamethod(LuaItemStack, add_wear_by_uses), + luamethod(LuaItemStack, get_wear_bar_params), luamethod(LuaItemStack, add_item), luamethod(LuaItemStack, item_fits), luamethod(LuaItemStack, take_item), diff --git a/src/script/lua_api/l_item.h b/src/script/lua_api/l_item.h index 55333ec73b60..243d17d721e5 100644 --- a/src/script/lua_api/l_item.h +++ b/src/script/lua_api/l_item.h @@ -125,6 +125,11 @@ class LuaItemStack : public ModApiBase, public IntrusiveReferenceCounted { // Returns true if the item is (or was) a tool. static int l_add_wear_by_uses(lua_State *L); + // get_wear_bar_params(self) -> table + // Returns the effective wear bar parameters. + // Returns nil if this item has none associated. + static int l_get_wear_bar_params(lua_State *L); + // add_item(self, itemstack or itemstring or table or nil) -> itemstack // Returns leftover item stack static int l_add_item(lua_State *L); diff --git a/src/script/lua_api/l_itemstackmeta.cpp b/src/script/lua_api/l_itemstackmeta.cpp index 8ce9673db4e2..ebabf7baec5f 100644 --- a/src/script/lua_api/l_itemstackmeta.cpp +++ b/src/script/lua_api/l_itemstackmeta.cpp @@ -22,6 +22,8 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "lua_api/l_itemstackmeta.h" #include "lua_api/l_internal.h" #include "common/c_content.h" +#include "common/c_converter.h" +#include "tool.h" /* ItemStackMetaRef @@ -58,6 +60,20 @@ int ItemStackMetaRef::l_set_tool_capabilities(lua_State *L) return 0; } +int ItemStackMetaRef::l_set_wear_bar_params(lua_State *L) +{ + ItemStackMetaRef *metaref = checkObject(L, 1); + if (lua_isnoneornil(L, 2)) { + metaref->clearWearBarParams(); + } else if (lua_istable(L, 2) || lua_isstring(L, 2)) { + metaref->setWearBarParams(read_wear_bar_params(L, 2)); + } else { + luaL_typerror(L, 2, "table, ColorString, or nil"); + } + + return 0; +} + ItemStackMetaRef::ItemStackMetaRef(LuaItemStack *istack): istack(istack) { istack->grab(); @@ -102,5 +118,6 @@ const luaL_Reg ItemStackMetaRef::methods[] = { luamethod(MetaDataRef, from_table), luamethod(MetaDataRef, equals), luamethod(ItemStackMetaRef, set_tool_capabilities), + luamethod(ItemStackMetaRef, set_wear_bar_params), {0,0} }; diff --git a/src/script/lua_api/l_itemstackmeta.h b/src/script/lua_api/l_itemstackmeta.h index ac27bcc2caf3..27adc57e09bf 100644 --- a/src/script/lua_api/l_itemstackmeta.h +++ b/src/script/lua_api/l_itemstackmeta.h @@ -49,8 +49,19 @@ class ItemStackMetaRef : public MetaDataRef istack->getItem().metadata.clearToolCapabilities(); } + void setWearBarParams(const WearBarParams ¶ms) + { + istack->getItem().metadata.setWearBarParams(params); + } + + void clearWearBarParams() + { + istack->getItem().metadata.clearWearBarParams(); + } + // Exported functions static int l_set_tool_capabilities(lua_State *L); + static int l_set_wear_bar_params(lua_State *L); public: // takes a reference ItemStackMetaRef(LuaItemStack *istack); diff --git a/src/tool.cpp b/src/tool.cpp index 220df24ac1b6..36ad1c608cc0 100644 --- a/src/tool.cpp +++ b/src/tool.cpp @@ -26,7 +26,10 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "convert_json.h" #include "util/serialize.h" #include "util/numeric.h" +#include "util/hex.h" +#include "common/c_content.h" #include + void ToolGroupCap::toJson(Json::Value &object) const { @@ -183,6 +186,127 @@ void ToolCapabilities::deserializeJson(std::istream &is) } } +void WearBarParams::serialize(std::ostream &os) const +{ + writeU8(os, 1); // Version for future-proofing + writeU8(os, blend); + writeU16(os, colorStops.size()); + for (const std::pair item : colorStops) { + writeF32(os, item.first); + writeARGB8(os, item.second); + } +} + +WearBarParams WearBarParams::deserialize(std::istream &is) +{ + u8 version = readU8(is); + if (version > 1) + throw SerializationError("unsupported WearBarParams version"); + + auto blend = static_cast(readU8(is)); + if (blend >= BlendMode_END) + throw SerializationError("invalid blend mode"); + u16 count = readU16(is); + if (count == 0) + throw SerializationError("no stops"); + std::map colorStops; + for (u16 i = 0; i < count; i++) { + f32 key = readF32(is); + if (key < 0 || key > 1) + throw SerializationError("key out of range"); + video::SColor color = readARGB8(is); + colorStops.emplace(key, color); + } + return WearBarParams(colorStops, blend); +} + +void WearBarParams::serializeJson(std::ostream &os) const +{ + Json::Value root; + Json::Value color_stops; + for (const std::pair item : colorStops) { + color_stops[ftos(item.first)] = encodeHexColorString(item.second); + } + root["color_stops"] = color_stops; + root["blend"] = WearBarParams::es_BlendMode[blend].str; + + fastWriteJson(root, os); +} + +std::optional WearBarParams::deserializeJson(std::istream &is) +{ + Json::Value root; + is >> root; + if (!root.isObject() || !root["color_stops"].isObject() || !root["blend"].isString()) + return std::nullopt; + + int blendInt; + WearBarParams::BlendMode blend; + if (string_to_enum(WearBarParams::es_BlendMode, blendInt, root["blend"].asString())) + blend = static_cast(blendInt); + else + return std::nullopt; + + const Json::Value &color_stops_object = root["color_stops"]; + std::map colorStops; + for (const std::string &key : color_stops_object.getMemberNames()) { + f32 stop = stof(key); + if (stop < 0 || stop > 1) + return std::nullopt; + const Json::Value &value = color_stops_object[key]; + if (value.isString()) { + video::SColor color; + parseColorString(value.asString(), color, false); + colorStops.emplace(stop, color); + } + } + if (colorStops.empty()) + return std::nullopt; + return WearBarParams(colorStops, blend); +} + +video::SColor WearBarParams::getWearBarColor(f32 durabilityPercent) { + if (colorStops.empty()) + return video::SColor(); + + /* + * Strategy: + * Find upper bound of durabilityPercent + * + * if it == stops.end() -> return last color in the map + * if it == stops.begin() -> return first color in the map + * + * else: + * lower_bound = it - 1 + * interpolate/do constant + */ + auto upper = colorStops.upper_bound(durabilityPercent); + + if (upper == colorStops.end()) // durability is >= the highest defined color stop + return std::prev(colorStops.end())->second; // return last element of the map + + if (upper == colorStops.begin()) // durability is <= the lowest defined color stop + return upper->second; + + auto lower = std::prev(upper); + f32 lower_bound = lower->first; + video::SColor lower_color = lower->second; + f32 upper_bound = upper->first; + video::SColor upper_color = upper->second; + + f32 progress = (durabilityPercent - lower_bound) / (upper_bound - lower_bound); + + switch (blend) { + case BLEND_MODE_CONSTANT: + return lower_color; + case BLEND_MODE_LINEAR: + return upper_color.getInterpolated(lower_color, progress); + case BlendMode_END: + throw std::logic_error("dummy value"); + } + throw std::logic_error("invalid blend value"); +} + u32 calculateResultWear(const u32 uses, const u16 initial_wear) { if (uses == 0) { diff --git a/src/tool.h b/src/tool.h index 8a22fca7b994..3121e907d40f 100644 --- a/src/tool.h +++ b/src/tool.h @@ -20,10 +20,15 @@ with this program; if not, write to the Free Software Foundation, Inc., #pragma once #include "irrlichttypes.h" -#include -#include #include "itemgroup.h" #include "json-forwards.h" +#include "common/c_types.h" +#include +#include + +#include +#include +#include struct ItemDefinition; @@ -82,6 +87,37 @@ struct ToolCapabilities void deserializeJson(std::istream &is); }; +struct WearBarParams +{ + std::map colorStops; + enum BlendMode: u8 { + BLEND_MODE_CONSTANT, + BLEND_MODE_LINEAR, + BlendMode_END // Dummy for validity check + }; + constexpr const static EnumString es_BlendMode[3] = { + {WearBarParams::BLEND_MODE_CONSTANT, "constant"}, + {WearBarParams::BLEND_MODE_LINEAR, "linear"}, + {0, NULL} + }; + BlendMode blend; + + WearBarParams(const std::map &colorStops, BlendMode blend): + colorStops(colorStops), + blend(blend) + {} + + WearBarParams(const video::SColor color): + WearBarParams({{0.0, color}}, WearBarParams::BLEND_MODE_CONSTANT) + {}; + + void serialize(std::ostream &os) const; + static WearBarParams deserialize(std::istream &is); + void serializeJson(std::ostream &os) const; + static std::optional deserializeJson(std::istream &is); + video::SColor getWearBarColor(f32 durabilityPercent); +}; + struct DigParams { bool diggable; diff --git a/src/util/string.cpp b/src/util/string.cpp index b4af7a404d52..efb07c1d1820 100644 --- a/src/util/string.cpp +++ b/src/util/string.cpp @@ -574,6 +574,20 @@ bool parseColorString(const std::string &value, video::SColor &color, bool quiet return success; } +std::string encodeHexColorString(const video::SColor &color) +{ + std::string color_string = "#"; + const char red = color.getRed(); + const char green = color.getGreen(); + const char blue = color.getBlue(); + const char alpha = color.getAlpha(); + color_string += hex_encode(&red, 1); + color_string += hex_encode(&green, 1); + color_string += hex_encode(&blue, 1); + color_string += hex_encode(&alpha, 1); + return color_string; +} + void str_replace(std::string &str, char from, char to) { std::replace(str.begin(), str.end(), from, to); diff --git a/src/util/string.h b/src/util/string.h index 5d49cc77d56f..f5382200dbb2 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -88,6 +88,7 @@ char *mystrtok_r(char *s, const char *sep, char **lasts) noexcept; u64 read_seed(const char *str); bool parseColorString(const std::string &value, video::SColor &color, bool quiet, unsigned char default_alpha = 0xff); +std::string encodeHexColorString(const video::SColor &color); /**