diff --git a/mods/amogus_blocks/init.lua b/mods/amogus_blocks/init.lua index df399af..62d4bb1 100644 --- a/mods/amogus_blocks/init.lua +++ b/mods/amogus_blocks/init.lua @@ -1,3 +1,7 @@ +minetest.register_on_joinplayer(function(player) + player:set_sky({r=0, g=0, b=0}, "plain", {}) +end) + minetest.register_node("amogus_blocks:ladder_steel", { description = "Ladder", drawtype = "signlike", @@ -25,12 +29,12 @@ minetest.register_node("amogus_blocks:floor", { minetest.register_node("amogus_blocks:tv", { description = "CRT TV", tiles = { - "PC2.png", - "PC2.png", - "PC2.png", - "PC2.png", - "PC2.png", - "PC.png" + "crt2.png", + "crt2.png", + "crt2.png", + "crt2.png", + "crt2.png", + "crt.png" }, paramtype2 = "facedir", drop = 'amogus_blocks:amogus_blocks', groups = {cracky=3, stone=1}, @@ -118,58 +122,5 @@ minetest.register_node("amogus_blocks:wood", { drop = 'amogus:wood', groups = {cracky=3, stone=1}, }) --- add water with blue color without texture -minetest.register_node("amogus_blocks:water", { - description = "Water", - drawtype = "liquid", - tiles = { - { - name = "water.png", - animation = { - type = "vertical_frames", - aspect_w = 16, - aspect_h = 16, - length = 2.0, - }, - }, - }, - special_tiles = { - -- New-style water source material (mostly unused) - { - name = "water.png", - backface_culling = false, - animation = { - type = "vertical_frames", - aspect_w = 16, - aspect_h = 16, - length = 2.0, - }, - }, - -- New-style flowing water material (mostly unused) - { - name = "water.png", - backface_culling = true, - animation = { - type = "vertical_frames", - aspect_w = 16, - aspect_h = 16, - length = 0.8, - }, - }, - }, - alpha = 160, - paramtype = "light", - walkable = false, - pointable = false, - diggable = false, - buildable_to = true, - drowning = 1, - liquidtype = "source", - liquid_alternative_flowing = "amogus_blocks:water", - liquid_alternative_source = "amogus_blocks:water", - liquid_viscosity = 1, - post_effect_color = {a = 103, r = 30, g = 60, b = 90}, - groups = {water = 3, liquid = 3, puts_out_fire = 1}, -}) -minetest.register_alias("mapgen_dirt", "amogus_blocks:grass") -minetest.register_alias("mapgen_stone", "amogus_blocks:floor") \ No newline at end of file +minetest.register_alias("mapgen_dirt", "amogus_blocks:floor") +minetest.register_alias("mapgen_stone", "amogus_blocks:grass") \ No newline at end of file diff --git a/mods/amogus_general/init.lua b/mods/amogus_general/init.lua new file mode 100644 index 0000000..e69de29 diff --git a/mods/amogus_generator/init.lua b/mods/amogus_generator/init.lua new file mode 100644 index 0000000..e69de29 diff --git a/mods/amogus_items/init.lua b/mods/amogus_items/init.lua index 1b2029c..621c313 100644 --- a/mods/amogus_items/init.lua +++ b/mods/amogus_items/init.lua @@ -56,9 +56,48 @@ minetest.register_craftitem("amogus_items:tomato", { on_use = minetest.item_eat(1), }) minetest.register_craftitem("amogus_items:water", { - description = "Water", inventory_image = "water.png", on_use = minetest.item_eat(1), }) --- add sprite, coke, fanta, tomato, and water crafting recipes +-- add lightsaber_blue and lightsaber_red and lightsaber_green and let it destroy nodes and mobs +minetest.register_tool("amogus_items:lightsaber_blue", { + description = "Blue Lightsaber", + inventory_image = "lightsaber_blue.png", + tool_capabilities = { + full_punch_interval = 0.1, + max_drop_level=3, + groupcaps={ + snappy={times={[1]=0.1, [2]=0.1, [3]=0.1}, uses=0, maxlevel=3}, + }, + damage_groups = {fleshy=10}, + }, + sound = {breaks = "amogus_sound"}, +}) +minetest.register_tool("amogus_items:lightsaber_red", { + description = "Red Lightsaber", + inventory_image = "lightsaber_red.png", + tool_capabilities = { + full_punch_interval = 0.1, + max_drop_level=3, + groupcaps={ + snappy={times={[1]=0.1, [2]=0.1, [3]=0.1}, uses=0, maxlevel=3}, + }, + damage_groups = {fleshy=10}, + }, + sound = {breaks = "amogus_sound"}, +}) + +minetest.register_tool("amogus_items:lightsaber_green", { + description = "Green Lightsaber", + inventory_image = "lightsaber_green.png", + tool_capabilities = { + full_punch_interval = 0.1, + max_drop_level=3, + groupcaps={ + snappy={times={[1]=0.1, [2]=0.1, [3]=0.1}, uses=0, maxlevel=3}, + }, + damage_groups = {fleshy=10}, + }, + sound = {breaks = "amogus_sound"}, +}) \ No newline at end of file diff --git a/mods/i3/.editorconfig b/mods/i3/.editorconfig new file mode 100644 index 0000000..89dddff --- /dev/null +++ b/mods/i3/.editorconfig @@ -0,0 +1,9 @@ +[*] +end_of_line = lf + +[*.{lua,txt,md,json}] +charset = utf8 +indent_size = 8 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/mods/i3/.github/ISSUE_TEMPLATE/bug_report.md b/mods/i3/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..96ac07f --- /dev/null +++ b/mods/i3/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +- Mod version? Release or git? +- Engine version? +- LuaJIT enabled? +- Operating system? +- Did you try to disable other mods except i3? diff --git a/mods/i3/.github/workflows/luacheck.yml b/mods/i3/.github/workflows/luacheck.yml new file mode 100644 index 0000000..d9d6abf --- /dev/null +++ b/mods/i3/.github/workflows/luacheck.yml @@ -0,0 +1,21 @@ +name: Luacheck + +on: [push, pull_request] + +jobs: + + luacheck: + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup ‘lua’ + uses: leafo/gh-actions-lua@v8 + with: + luaVersion: 5.1 + - name: Setup ‘luarocks’ + uses: leafo/gh-actions-luarocks@v4 + - name: Setup ‘luacheck’ + run: luarocks install luacheck + - name: Run ‘luacheck’ linter + run: cd util; lua luacheck.lua diff --git a/mods/i3/.gitignore b/mods/i3/.gitignore new file mode 100644 index 0000000..6fd0a37 --- /dev/null +++ b/mods/i3/.gitignore @@ -0,0 +1,41 @@ +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + diff --git a/mods/i3/.luacheckrc b/mods/i3/.luacheckrc new file mode 100644 index 0000000..79da584 --- /dev/null +++ b/mods/i3/.luacheckrc @@ -0,0 +1,37 @@ +allow_defined_top = true + +ignore = { + "631", -- Line is too long. + "get_debug_grid", +} + +read_globals = { + "minetest", + "armor", + "skins", + "awards", + "hb", + "vector", + "string", + "table", + "ItemStack", + "VoxelArea", + "VoxelManip", +} + +globals = { + "i3", + "core", + "sfinv", + "unified_inventory", +} + +exclude_files = { + "tests/test_compression.lua", + "tests/test_custom_recipes.lua", + "tests/test_operators.lua", + "tests/test_tabs.lua", + + ".install", + ".luarocks", +} diff --git a/mods/i3/API.md b/mods/i3/API.md new file mode 100644 index 0000000..6d22301 --- /dev/null +++ b/mods/i3/API.md @@ -0,0 +1,338 @@ +## API + +### Custom tabs + +#### `i3.new_tab(name, def)` + +- `name` is the tab name. +- `def` is the tab definition. + +Custom tabs can be added to the `i3` inventory as follow (example): + +```Lua +i3.new_tab("stuff", { + description = "Stuff", + image = "image.png", -- Optional, add an image next to the tab description + + -- + -- The functions below are all optional + -- + + -- Determine if the tab is visible by a player, return false to hide the tab + access = function(player, data) + local name = player:get_player_name() + return name == "singleplayer" + end, + + formspec = function(player, data, fs) + fs("label", 3, 1, "Just a test") + fs"label[3,2;Lorem Ipsum]" + -- No need to return anything + end, + + -- Events handling happens here + fields = function(player, data, fields) + if fields.mybutton then + -- Do things + end + + i3.set_fs(player) -- Update the formspec, mandatory + end, +}) +``` + +- `player` is an `ObjectRef` to the user. +- `data` are the user data. +- `fs` is the formspec table which is callable with a metamethod. Every call adds a new entry. + +#### `i3.set_fs(player)` + +Update the current formspec. + +#### `i3.remove_tab(tabname)` + +Delete a tab by name. + +#### `i3.get_current_tab(player)` + +Return the current player tab. `player` is an `ObjectRef` to the user. + +#### `i3.set_tab(player[, tabname])` + +Sets the current tab by name. `player` is an `ObjectRef` to the user. +`tabname` can be omitted to get an empty tab. + +#### `i3.override_tab(tabname, def)` + +Override a tab by name. `def` is the tab definition like seen in `i3.set_tab` + +#### `i3.tabs` + +A list of registered tabs. + +--- + +### Custom recipes + +Custom recipes are nonconventional crafts outside the main crafting grid. +They can be registered in-game dynamically and have a size beyond 3x3 items. + +**Note:** the registration format differs from the default registration format in everything. +The width is automatically calculated depending where you place the commas. + +Examples: + +#### Registering a custom crafting type + +```Lua +i3.register_craft_type("digging", { + description = "Digging", + icon = "default_tool_steelpick.png", +}) +``` + +#### Registering a custom crafting recipe + +```Lua +i3.register_craft { + type = "digging", + result = "default:cobble 2", + items = {"default:stone"}, +} +``` + +```Lua +i3.register_craft { + result = "default:cobble 16", + items = { + "default:stone, default:stone, default:stone", + "default:stone, , default:stone", + "default:stone, default:stone, default:stone", + } +} +``` + +Recipes can be registered in a Minecraft-like way: + +```Lua +i3.register_craft { + grid = { + "X #", + " ## ", + "X#X#", + "X X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass", + }, + result = "default:mese 3", +} +``` + +Multiples recipes can also be registered: + +```Lua +i3.register_craft { + { + result = "default:mese", + items = { + "default:mese_crystal, default:mese_crystal", + "default:mese_crystal, default:mese_crystal", + } + }, + + big = { + result = "default:mese 4", + items = { + "default:mese_crystal, default:mese_crystal", + "default:mese_crystal, default:mese_crystal", + "default:mese_crystal, default:mese_crystal", + "default:mese_crystal, default:mese_crystal", + } + }, +} +``` + +Recipes can be registered from a given URL containing a JSON file (HTTP support is required¹): + +```Lua +i3.register_craft { + url = "https://raw.githubusercontent.com/minetest-mods/i3/main/tests/test_online_recipe.json" +} +``` + +--- + +### Recipe filters + +Recipe filters can be used to filter the recipes shown to players. Progressive +mode is implemented as a recipe filter. + +#### `i3.add_recipe_filter(name, function(recipes, player))` + +Add a recipe filter with the given `name`. The filter function returns the +recipes to be displayed, given the available recipes and an `ObjectRef` to the +user. Each recipe is a table of the form returned by +`minetest.get_craft_recipe`. + +Example function to hide recipes for items from a mod called "secretstuff": + +```lua +i3.add_recipe_filter("Hide secretstuff", function(recipes) + local filtered = {} + for _, recipe in ipairs(recipes) do + if recipe.output:sub(1,12) ~= "secretstuff:" then + filtered[#filtered + 1] = recipe + end + end + + return filtered +end) +``` + +#### `i3.set_recipe_filter(name, function(recipe, player))` + +Remove all recipe filters and add a new one. + +#### `i3.recipe_filters` + +A map of recipe filters, indexed by name. + +--- + +### Search filters + +Search filters are used to perform specific searches from the search field. +The filters can be cumulated to perform a specific search. +They are used like so: ` +=,,<...>` + +Example usages: + +- `+groups=cracky,crumbly` -> search for groups `cracky` and `crumbly` in all items. +- `wood +groups=flammable` -> search for group `flammable` amongst items which contain + `wood` in their names. + +Notes: +- If `optional_name` is omitted, the search filter will apply to all items, without pre-filtering. +- The `+groups` filter is currently implemented by default. + +#### `i3.add_search_filter(name, function(item, values))` + +Add a search filter. +The search function must return a boolean value (whether the given item should be listed or not). + +- `name` is the filter name. +- `values` is a table of all possible values. + +Example function sorting items by drawtype: + +```lua +i3.add_search_filter("types", function(item, drawtypes) + local t = {} + + for i, dt in ipairs(drawtypes) do + t[i] = (dt == "node" and reg_nodes[item] and 1) or + (dt == "item" and reg_craftitems[item] and 1) or + (dt == "tool" and reg_tools[item] and 1) or nil + end + + return #t > 0 +end) +``` + +#### `i3.search_filters` + +A map of search filters, indexed by name. + +--- + +### Sorting methods + +Sorting methods are used to filter the player's main inventory. + +#### `i3.add_sorting_method(name, def)` + +Add a player inventory sorting method. + +- `name` is the method name. +- `def` is the method definition. + +Example: + +```Lua +i3.add_sorting_method("test", { + description = "Cool sorting method", + func = function(list, data) + -- `list`: inventory list + -- `data`: player data + + table.sort(list) + + -- A list must be returned + return list + end, +}) + +``` + +#### `i3.sorting_methods` + +A table containing all sorting methods. + +--- + +### Item list compression + +`i3` can reduce the item list size by compressing a group of items. + +#### `i3.compress(item, def)` + +Add a new group of items to compress. + +- `item` is the item which represent the group of compressed items. +- `def` is a table specifying the substring replace patterns to be used. + +Example: + +```Lua +i3.compress("default:diamondblock", { + replace = "diamond", + by = {"bronze", "copper", "gold", "steel", "tin"} +}) + +``` + +#### `i3.compress_groups` + +A map of all compressed item groups, indexed by stereotypes. + +--- + +### Miscellaneous + +#### `i3.hud_notif(name, msg[, img])` + +Show a Steam-like HUD notification on the bottom-right corner of the screen (experimental). + +- `name` is the player name. +- `msg` is the HUD message to show. +- `img` (optional) is the HUD image to show (preferably 16x16 px). + +#### `i3.get_recipes(item)` + +Return a table of recipes and usages of `item`. + +#### `i3.export_url` + +If set, the mod will export all the cached recipes and usages in a JSON format +to the given URL (HTTP support is required¹). + +#### `groups = {bag = <1-4>}` + +The `bag` group in the item definition allows to extend the player inventory size +given a number between 1 and 4. + +--- + +**¹** Add `i3` to the `secure.http_mods` or `secure.trusted_mods` setting in `minetest.conf`. diff --git a/mods/i3/LICENSE b/mods/i3/LICENSE new file mode 100644 index 0000000..1abebab --- /dev/null +++ b/mods/i3/LICENSE @@ -0,0 +1,60 @@ +License of source code +---------------------- + +The MIT License (MIT) + +Copyright (c) 2020-2021 Jean-Patrick Guerrero and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +Licenses of media (textures) +---------------------------- + +paramat (CC BY-SA 3.0): + i3_arrow.png - derived from a texture by BlockMen (CC BY-SA 3.0) + i3_hotbar.png + +You are free to: +Share — copy and redistribute the material in any medium or format. +Adapt — remix, transform, and build upon the material for any purpose, even commercially. +The licensor cannot revoke these freedoms as long as you follow the license terms. + +Under the following terms: + +Attribution — You must give appropriate credit, provide a link to the license, and +indicate if changes were made. You may do so in any reasonable manner, but not in any way +that suggests the licensor endorses you or your use. + +ShareAlike — If you remix, transform, or build upon the material, you must distribute +your contributions under the same license as the original. + +No additional restrictions — You may not apply legal terms or technological measures that +legally restrict others from doing anything the license permits. + +Notices: + +You do not have to comply with the license for elements of the material in the public +domain or where your use is permitted by an applicable exception or limitation. +No warranties are given. The license may not give you all of the permissions necessary +for your intended use. For example, other rights such as publicity, privacy, or moral +rights may limit how you use the material. + +For more details: +http://creativecommons.org/licenses/by-sa/3.0/ diff --git a/mods/i3/README.md b/mods/i3/README.md new file mode 100644 index 0000000..6d4f702 --- /dev/null +++ b/mods/i3/README.md @@ -0,0 +1,58 @@ +![logo](https://user-images.githubusercontent.com/7883281/145490041-d91d6bd6-a654-438d-b208-4d5736845ab7.png) + +[![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/tterb/atomic-design-ui/blob/master/LICENSEs) [![GitHub Release](https://img.shields.io/github/release/minetest-mods/i3.svg?style=flat)]() ![workflow](https://github.com/minetest-mods/i3/actions/workflows/luacheck.yml/badge.svg) [![ContentDB](https://content.minetest.net/packages/jp/i3/shields/downloads/)](https://content.minetest.net/packages/jp/i3/) [![PayPal](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.me/jpg84240) + +#### **`i3`** is a next-generation inventory for Minetest. + +This mod features a modern, powerful inventory menu with a good user experience. +**`i3`** provides a rich [**API**](https://github.com/minetest-mods/i3/blob/master/API.md) for mod developers who want to extend it. + +This mod requires **Minetest 5.6+** + +#### List of features: + - Crafting Guide (survival mode only) + - Progressive Mode¹ + - Quick Crafting + - 3D Player Model Real-Time Preview + - Isometric Map Preview + - Inventory Sorting (+ options: compression, reverse mode, automation, etc.) + - Item List Compression (**`moreblocks`** is supported) + - Item Bookmarks + - Waypoints + - Bags + - Home + +**¹** *This mode is a Terraria-like system that shows recipes you can craft from items you ever had in your inventory. +To enable it: `i3_progressive_mode = true` in `minetest.conf`.* + +#### This mod officially supports the following mods: + - [**`3d_armor`**](https://content.minetest.net/packages/stu/3d_armor/) + - [**`skinsdb`**](https://content.minetest.net/packages/bell07/skinsdb/) + - [**`awards`**](https://content.minetest.net/packages/rubenwardy/awards/) + +#### Recommendations + +To use this mod in the best conditions: + + - Use LuaJIT + - Use a HiDPI widescreen display + - Use the default Freetype font style + +#### Troubleshooting + +If the inventory's font size is too big on certain setups (namely Windows 10/11 or 144 DPI display), you should lower the +value of the setting `display_density_factor` in your `minetest.conf`. Note that the change is applied after restart. + +You can also use the font size slider in the inventory, settings window. + +#### Notes + +`i3` uses a larger inventory than the usual inventories in Minetest games. +Thus, most chests will be unadapted to this inventory size. +The `i3` inventory is 9 slots wide by default, such as Minecraft. + +Report bugs on the [**Bug Tracker**](https://github.com/minetest-mods/i3/issues). + +**Video review on YouTube:** https://www.youtube.com/watch?v=Xd14BCdEZ3o + +![Preview](https://user-images.githubusercontent.com/7883281/185755315-23c2fffa-203d-4115-9dc3-576c92615733.png) diff --git a/mods/i3/init.lua b/mods/i3/init.lua new file mode 100644 index 0000000..1ad3cbf --- /dev/null +++ b/mods/i3/init.lua @@ -0,0 +1,127 @@ +print[[ + + Powered by + + ██╗██████╗ + ██║╚════██╗ + ██║ █████╔╝ + ██║ ╚═══██╗ + ██║██████╔╝ + ╚═╝╚═════╝ +]] + +local modpath = core.get_modpath"i3" +local http = core.request_http_api() +local storage = core.get_mod_storage() +local _loadfile = dofile(modpath .. "/src/preprocessor.lua") + +local function lf(path) + return assert(_loadfile(modpath .. path)) +end + +i3 = { + version = 1123, + data = core.deserialize(storage:get_string"data") or {}, + + settings = { + debug_mode = false, + max_favs = 6, + max_waypoints = 30, + min_fs_version = 6, + item_btn_size = 1.1, + drop_bag_on_die = true, + wielditem_fade_after = 3, + save_interval = 600, -- Player data save interval (in seconds) + + hud_speed = 1, + hud_timer_max = 1.5, + + damage_enabled = core.settings:get_bool"enable_damage", + progressive_mode = core.settings:get_bool"i3_progressive_mode", + }, + + categories = { + "bag", + "armor", + "skins", + "awards", + "waypoints", + }, + + saves = { -- Metadata to save + bag = true, + home = true, + sort = true, + collapse = true, + font_size = true, + hide_tabs = true, + waypoints = true, + inv_items = true, + auto_sorting = true, + inv_compress = true, + known_recipes = true, + wielditem_hud = true, + ignore_hotbar = true, + reverse_sorting = true, + legacy_inventory = true, + }, + + default_data = { + sort = 1, + font_size = 0, + collapse = true, + inv_compress = true, + }, + + files = { + api = lf"/src/api.lua", + bags = lf"/src/bags.lua", + caches = lf"/src/caches.lua", + callbacks = lf"/src/callbacks.lua", + common = lf"/src/common.lua", + compress = lf"/src/compression.lua", + detached = lf"/src/detached_inv.lua", + fields = lf"/src/fields.lua", + groups = lf"/src/groups.lua", + gui = lf"/src/gui.lua", + hud = lf"/src/hud.lua", + model_alias = lf"/src/model_aliases.lua", + progressive = lf"/src/progressive.lua", + styles = lf"/src/styles.lua", + }, + + -- Caches + init_items = {}, + fuel_cache = {}, + usages_cache = {}, + recipes_cache = {}, + + tabs = {}, + cubes = {}, + groups = {}, + plants = {}, + modules = {}, + craft_types = {}, + + recipe_filters = {}, + search_filters = {}, + sorting_methods = {}, +} + +i3.files.common() +i3.files.api(http) +i3.files.compress() +i3.files.detached() +i3.files.groups() +i3.files.callbacks(http, storage) + +if i3.settings.progressive_mode then + i3.files.progressive() +end + +if i3.settings.debug_mode then + lf("/tests/test_tabs.lua")() +-- lf("/tests/test_operators.lua")() + lf("/tests/test_compression.lua")() + lf("/tests/test_custom_recipes.lua")() +end diff --git a/mods/i3/locale/i3.fr.tr b/mods/i3/locale/i3.fr.tr new file mode 100644 index 0000000..2c57546 --- /dev/null +++ b/mods/i3/locale/i3.fr.tr @@ -0,0 +1,85 @@ +# textdomain: i3 + +### init.lua ### + +@1 added in your inventory=@1 ajouté à votre inventaire +@1 new recipe(s) discovered!=@1 nouvelle(s) recette(s) découverte(s)! +@1 of chance to drop=@1 de chance de tomber +@1 spawned=@1 spawné +Achievements: @1 of @2 (@3)=Succès : @1 sur @2 (@3) +Any black dye=N'importe quel colorant noir +Any black flower=N'importe quelle fleur noire +Any blue dye=N'importe quel colorant bleu +Any blue flower=N'importe quelle fleur bleue +Any brown dye=N'importe quel colorant marron +Any carpet=N'importe quel tapis +Any coal=N'importe quel charbon +Any cyan dye=N'importe quel colorant bleu turquoise +Any dark green dye=N'importe quel colorant vert foncé +Any dark grey dye=N'importe quel colorant gris foncé +Any dye=N'importe quel colorant +Any flower=N'importe quelle fleur +Any glass=N'importe quel verre +Any green dye=N'importe quel colorant vert +Any green flower=N'importe quelle fleur verte +Any grey dye=N'importe quel colorant gris +Any item belonging to the groups: @1=Tout item appartenant aux groupes : @1 +Any leaves=N'importe quelles feuilles d'arbre +Any magenta dye=N'importe quel colorant mauve +Any mushroom=N'importe quel champignon +Any orange dye=N'importe quel colorant orange +Any orange flower=N'importe quelle fleur orange +Any pink dye=N'importe quel colorant rose +Any red dye=N'importe quel colorant rouge +Any red flower=N'importe quelle fleur rouge +Any sand=N'importe quel sable +Any stick=N'importe quel bâton +Any stone=N'importe quelle roche +Any tree=N'importe quel tronc d'arbre +Any vessel=N'importe quel couvert +Any violet dye=N'importe quel colorant violet +Any violet flower=N'importe quelle fleur violette +Any white dye=N'importe quel colorant blanc +Any white flower=N'importe quelle fleur blanche +Any wood planks=N'importe quelles planches de bois +Any wool=N'importe quelle laine +Any yellow dye=N'importe quel colorant jaune +Any yellow flower=N'importe quelle fleur jaune +Armor=Armure +Bag=Sac +Bookmarks=Favoris +Burning time: @1=Temps de combustion : @1 +Cannot mark this item. Bookmark limit reached.= +Collect items to reveal more recipes=Collecter des items pour révéler des recettes +Compress items=Compresser les items +Cooking=Cuisson +Cooking time: @1=Temps de cuisson : @1 +Craft (x@1)=Fabriquer (x@1) +Digging=Minage +Digging (by chance)=Minage (par chance) +Heal=Guérison +Inventory=Inventaire +Items=Items +Level=Niveau +Mark this item=Ajouter aux favoris +No item to show=Aucun item à montrer +No recipes=Aucune recette +No usages=Aucun usage +Only drop if using one of these tools: @1=Tombe seulement en utilisant un de ces outils : @1 +Only drop if using this tool: @1=Tombe seulement en utilisant cet outil : @1 +Quick crafting=Fabrication rapide +Recipe @1 of @2=Recette @1 sur @2 +Repairable by step of @1=Réparable par étape de @1 +Replaced by @1 on burning=Remplacé par @1 à la combustion +Replaced by @1 on crafting=Remplacé par @1 à la fabrication +Replaced by @1 on smelting=Remplacé par @1 à la cuisson +Shapeless=Sans forme particulière +Skins=Skins +Sort items (A-Z)=Trier les items (A-Z) +Sort items (Z-A)=Trier les items (Z-A) +The inventory is extended by @1 slots=L'inventaire est étendu de @1 slots +Trash all items=Détruire tous les items +Unknown Item (@1)=Item inconnu (@1) +Unmark this item=Enlever des favoris +Usage @1 of @2=Usage @1 sur @2 + \ No newline at end of file diff --git a/mods/i3/locale/template b/mods/i3/locale/template new file mode 100644 index 0000000..1046d60 --- /dev/null +++ b/mods/i3/locale/template @@ -0,0 +1,84 @@ +# textdomain: i3 + +### init.lua ### + +@1 added in your inventory= +@1 new recipe(s) discovered!= +@1 of chance to drop= +@1 spawned= +Achievements: @1 of @2 (@3)= +Any black dye= +Any black flower= +Any blue dye= +Any blue flower= +Any brown dye= +Any carpet= +Any coal= +Any cyan dye= +Any dark green dye= +Any dark grey dye= +Any dye= +Any flower= +Any glass= +Any green dye= +Any green flower= +Any grey dye= +Any item belonging to the groups: @1= +Any leaves= +Any magenta dye= +Any mushroom= +Any orange dye= +Any orange flower= +Any pink dye= +Any red dye= +Any red flower= +Any sand= +Any stick= +Any stone= +Any tree= +Any vessel= +Any violet dye= +Any violet flower= +Any white dye= +Any white flower= +Any wood planks= +Any wool= +Any yellow dye= +Any yellow flower= +Armor= +Bag= +Bookmarks= +Burning time: @1= +Cannot mark this item. Bookmark limit reached.= +Collect items to reveal more recipes= +Compress items= +Cooking= +Cooking time: @1= +Craft (x@1)= +Digging= +Digging (by chance)= +Heal= +Inventory= +Items= +Level= +Mark this item= +No item to show= +No recipes= +No usages= +Only drop if using one of these tools: @1= +Only drop if using this tool: @1= +Quick crafting= +Recipe @1 of @2= +Repairable by step of @1= +Replaced by @1 on burning= +Replaced by @1 on crafting= +Replaced by @1 on smelting= +Shapeless= +Skins= +Sort items (A-Z)= +Sort items (Z-A)= +The inventory is extended by @1 slots= +Trash all items= +Unknown Item (@1)= +Unmark this item= +Usage @1 of @2= diff --git a/mods/i3/mod.conf b/mods/i3/mod.conf new file mode 100644 index 0000000..04452f7 --- /dev/null +++ b/mods/i3/mod.conf @@ -0,0 +1,4 @@ +name = i3 +description = Next-generation inventory +optional_depends = 3d_armor, skinsdb, awards +min_minetest_version = 5.6 diff --git a/mods/i3/settingtypes.txt b/mods/i3/settingtypes.txt new file mode 100644 index 0000000..a97f54a --- /dev/null +++ b/mods/i3/settingtypes.txt @@ -0,0 +1,2 @@ +# The progressive mode shows recipes you can craft from items you ever had in your inventory. +i3_progressive_mode (Learn crafting recipes progressively) bool false diff --git a/mods/i3/sounds/i3_achievement.ogg b/mods/i3/sounds/i3_achievement.ogg new file mode 100644 index 0000000..df38d5b Binary files /dev/null and b/mods/i3/sounds/i3_achievement.ogg differ diff --git a/mods/i3/sounds/i3_cannot.ogg b/mods/i3/sounds/i3_cannot.ogg new file mode 100644 index 0000000..8f97186 Binary files /dev/null and b/mods/i3/sounds/i3_cannot.ogg differ diff --git a/mods/i3/sounds/i3_click.ogg b/mods/i3/sounds/i3_click.ogg new file mode 100644 index 0000000..2983f59 Binary files /dev/null and b/mods/i3/sounds/i3_click.ogg differ diff --git a/mods/i3/sounds/i3_craft.ogg b/mods/i3/sounds/i3_craft.ogg new file mode 100644 index 0000000..bdaa95e Binary files /dev/null and b/mods/i3/sounds/i3_craft.ogg differ diff --git a/mods/i3/sounds/i3_heavy_armor.ogg b/mods/i3/sounds/i3_heavy_armor.ogg new file mode 100644 index 0000000..fe89ff2 Binary files /dev/null and b/mods/i3/sounds/i3_heavy_armor.ogg differ diff --git a/mods/i3/sounds/i3_heavy_boots.ogg b/mods/i3/sounds/i3_heavy_boots.ogg new file mode 100644 index 0000000..f9b0965 Binary files /dev/null and b/mods/i3/sounds/i3_heavy_boots.ogg differ diff --git a/mods/i3/sounds/i3_heavy_helmet.ogg b/mods/i3/sounds/i3_heavy_helmet.ogg new file mode 100644 index 0000000..9755b15 Binary files /dev/null and b/mods/i3/sounds/i3_heavy_helmet.ogg differ diff --git a/mods/i3/sounds/i3_heavy_leggings.ogg b/mods/i3/sounds/i3_heavy_leggings.ogg new file mode 100644 index 0000000..f0ad665 Binary files /dev/null and b/mods/i3/sounds/i3_heavy_leggings.ogg differ diff --git a/mods/i3/sounds/i3_heavy_shield.ogg b/mods/i3/sounds/i3_heavy_shield.ogg new file mode 100644 index 0000000..65621ad Binary files /dev/null and b/mods/i3/sounds/i3_heavy_shield.ogg differ diff --git a/mods/i3/sounds/i3_light_armor.ogg b/mods/i3/sounds/i3_light_armor.ogg new file mode 100644 index 0000000..c9b49fa Binary files /dev/null and b/mods/i3/sounds/i3_light_armor.ogg differ diff --git a/mods/i3/sounds/i3_light_boots.ogg b/mods/i3/sounds/i3_light_boots.ogg new file mode 100644 index 0000000..04e2c5f Binary files /dev/null and b/mods/i3/sounds/i3_light_boots.ogg differ diff --git a/mods/i3/sounds/i3_light_helmet.ogg b/mods/i3/sounds/i3_light_helmet.ogg new file mode 100644 index 0000000..ca09535 Binary files /dev/null and b/mods/i3/sounds/i3_light_helmet.ogg differ diff --git a/mods/i3/sounds/i3_light_leggings.ogg b/mods/i3/sounds/i3_light_leggings.ogg new file mode 100644 index 0000000..b4371ee Binary files /dev/null and b/mods/i3/sounds/i3_light_leggings.ogg differ diff --git a/mods/i3/sounds/i3_light_shield.ogg b/mods/i3/sounds/i3_light_shield.ogg new file mode 100644 index 0000000..e461a36 Binary files /dev/null and b/mods/i3/sounds/i3_light_shield.ogg differ diff --git a/mods/i3/sounds/i3_skin_change.ogg b/mods/i3/sounds/i3_skin_change.ogg new file mode 100644 index 0000000..5ef190d Binary files /dev/null and b/mods/i3/sounds/i3_skin_change.ogg differ diff --git a/mods/i3/sounds/i3_tab.ogg b/mods/i3/sounds/i3_tab.ogg new file mode 100644 index 0000000..1ed4380 Binary files /dev/null and b/mods/i3/sounds/i3_tab.ogg differ diff --git a/mods/i3/sounds/i3_teleport.ogg b/mods/i3/sounds/i3_teleport.ogg new file mode 100644 index 0000000..882e9ee Binary files /dev/null and b/mods/i3/sounds/i3_teleport.ogg differ diff --git a/mods/i3/sounds/i3_trash.ogg b/mods/i3/sounds/i3_trash.ogg new file mode 100644 index 0000000..cc6aaaf Binary files /dev/null and b/mods/i3/sounds/i3_trash.ogg differ diff --git a/mods/i3/src/api.lua b/mods/i3/src/api.lua new file mode 100644 index 0000000..fb8ba00 --- /dev/null +++ b/mods/i3/src/api.lua @@ -0,0 +1,360 @@ +local http = ... +local make_fs, get_inventory_fs = i3.files.gui() + +IMPORT("gmatch", "split") +IMPORT("S", "err", "fmt", "reg_items") +IMPORT("sorter", "sort_inventory", "play_sound") +IMPORT("sort", "concat", "copy", "insert", "remove") +IMPORT("true_str", "true_table", "is_str", "is_func", "is_table", "clean_name") + +function i3.register_craft_type(name, def) + if not true_str(name) then + return err "i3.register_craft_type: name missing" + elseif not true_table(def) then + return err "i3.register_craft_type: definition missing" + elseif not is_str(def.description) then + def.description = "" + end + + i3.craft_types[name] = def +end + +function i3.register_craft(def) + local width, c = 0, 0 + + if http and true_str(def.url) then + http.fetch({url = def.url}, function(result) + if result.succeeded then + local t = core.parse_json(result.data) + if is_table(t) then + return i3.register_craft(t) + end + end + end) + + return + end + + if not true_table(def) then + return err "i3.register_craft: craft definition missing" + end + + if #def > 1 then + for _, v in pairs(def) do + i3.register_craft(v) + end + return + end + + if def.result then + def.output = def.result -- Backward compatibility + def.result = nil + end + + if not true_str(def.output) and not def.url then + return err "i3.register_craft: output missing" + end + + if not is_table(def.items) then + def.items = {} + end + + if def.grid then + if not is_table(def.grid) then + def.grid = {} + end + + if not is_table(def.key) then + def.key = {} + end + + local cp = copy(def.grid) + sort(cp, function(a, b) return #a > #b end) + + width = #cp[1] + + for i = 1, #def.grid do + while #def.grid[i] < width do + def.grid[i] = def.grid[i] .. " " + end + end + + for symbol in gmatch(concat(def.grid), ".") do + c++ + def.items[c] = def.key[symbol] + end + else + local items = copy(def.items) + local lines = {} + def.items = {} + + for i = 1, #items do + lines[i] = split(items[i], ",", true) + + if #lines[i] > width then + width = #lines[i] + end + end + + for i = 1, #items do + while #lines[i] < width do + insert(lines[i], items[i]) + end + end + + for _, line in ipairs(lines) do + for _, v in ipairs(line) do + c++ + def.items[c] = clean_name(v) + end + end + end + + local item = ItemStack(def.output):get_name() + i3.recipes_cache[item] = i3.recipes_cache[item] or {} + + def.custom = true + def.width = width + + insert(i3.recipes_cache[item], def) +end + +function i3.add_recipe_filter(name, f) + if not true_str(name) then + return err "i3.add_recipe_filter: name missing" + elseif not is_func(f) then + return err "i3.add_recipe_filter: function missing" + end + + i3.recipe_filters[name] = f +end + +function i3.set_recipe_filter(name, f) + if not is_str(name) then + return err "i3.set_recipe_filter: name missing" + elseif not is_func(f) then + return err "i3.set_recipe_filter: function missing" + end + + i3.recipe_filters = {[name] = f} +end + +function i3.add_search_filter(name, f) + if not true_str(name) then + return err "i3.add_search_filter: name missing" + elseif not is_func(f) then + return err "i3.add_search_filter: function missing" + end + + i3.search_filters[name] = f +end + +function i3.get_recipes(item) + item = core.registered_aliases[item] or item + local recipes = i3.recipes_cache[item] + local usages = i3.usages_cache[item] + + return {recipes = recipes, usages = usages} +end + +function i3.set_fs(player) + if not player or player.is_fake_player then return end + local name = player:get_player_name() + local data = i3.data[name] + if not data then return end + + if data.auto_sorting then + sort_inventory(player, data) + end + + for i, tab in ipairs(i3.tabs) do + if data.tab == i and tab.access and not tab.access(player, data) then + data.tab = 1 + break + end + end + + local fs = make_fs(player, data) + player:set_inventory_formspec(fs) +end + +function i3.new_tab(name, def) + if not true_str(name) then + return err "i3.new_tab: tab name missing" + elseif not true_table(def) then + return err "i3.new_tab: tab definition missing" + elseif not true_str(def.description) then + return err "i3.new_tab: description missing" + elseif #i3.tabs == 6 then + return err(fmt("i3.new_tab: cannot add '%s' tab. Limit reached (6).", def.name)) + end + + def.name = name + insert(i3.tabs, def) +end + +i3.new_tab("inventory", { + description = S"Inventory", + formspec = get_inventory_fs, + fields = i3.files.fields(), +}) + +function i3.remove_tab(name) + if not true_str(name) then + return err "i3.remove_tab: tab name missing" + end + + for i, def in ipairs(i3.tabs) do + if name == def.name then + remove(i3.tabs, i) + break + end + end +end + +function i3.get_current_tab(player) + local name = player:get_player_name() + local data = i3.data[name] + + return data.tab +end + +function i3.set_tab(player, tabname) + local name = player:get_player_name() + local data = i3.data[name] + + if not tabname or tabname == "" then + data.tab = 0 + return + end + + for i, tab in ipairs(i3.tabs) do + if tab.name == tabname then + data.tab = i + return + end + end + + err(fmt("i3.set_tab: tab name '%s' does not exist", tabname)) +end + +function i3.override_tab(name, newdef) + if not true_str(name) then + return err "i3.override_tab: tab name missing" + elseif not true_table(newdef) then + return err "i3.override_tab: tab definition missing" + elseif not true_str(newdef.description) then + return err "i3.override_tab: description missing" + end + + newdef.name = name + + for i, def in ipairs(i3.tabs) do + if def.name == name then + i3.tabs[i] = newdef + break + end + end +end + +i3.register_craft_type("digging", { + description = S"Digging", + icon = "i3_steelpick.png", +}) + +i3.register_craft_type("digging_chance", { + description = S"Digging (by chance)", + icon = "i3_mesepick.png", +}) + +i3.add_search_filter("groups", function(item, groups) + local def = reg_items[item] + local has_groups = true + + for _, group in ipairs(groups) do + if not def.groups[group] then + has_groups = nil + break + end + end + + return has_groups +end) + +function i3.compress(item, def) + if not true_str(item) then + return err "i3.compress: item name missing" + elseif not true_table(def) then + return err "i3.compress: replace definition missing" + elseif not true_str(def.replace) then + return err "i3.compress: replace string missing" + elseif not is_table(def.by) then + return err "i3.compress: replace substrings missing" + elseif i3.compressed[item] then + return err(fmt("i3.compress: item '%s' is already compressed", item)) + end + + local t = {} + i3.compress_groups[item] = i3.compress_groups[item] or {} + + for _, str in ipairs(def.by) do + local it = item:gsub(def.replace, str) + + insert(t, it) + insert(i3.compress_groups[item], it) + + i3.compressed[it] = true + end +end + +function i3.hud_notif(name, msg, img) + if not true_str(name) then + return err "i3.hud_notif: player name missing" + elseif not true_str(msg) then + return err "i3.hud_notif: message missing" + end + + local data = i3.data[name] + + if not data then + return err "i3.hud_notif: no player data initialized" + end + + data.show_hud = true + data.hud_msg = msg + + play_sound(name, "i3_achievement", 1.0) + + if img then + data.hud_img = fmt("%s^[resize:64x64", img) + end +end + +function i3.add_sorting_method(name, def) + if not true_str(name) then + return err "i3.add_sorting_method: name missing" + elseif not true_table(def) then + return err "i3.add_sorting_method: definition missing" + elseif not is_func(def.func) then + return err "i3.add_sorting_method: function missing" + end + + def.name = name + insert(i3.sorting_methods, def) +end + +i3.add_sorting_method("alphabetical", { + description = S"Sort items by name (A-Z)", + func = function(list, data) + sorter(list, data, 1) + return list + end +}) + +i3.add_sorting_method("numerical", { + description = S"Sort items by number of items per stack", + func = function(list, data) + sorter(list, data, 2) + return list + end, +}) diff --git a/mods/i3/src/bags.lua b/mods/i3/src/bags.lua new file mode 100644 index 0000000..5b3deca --- /dev/null +++ b/mods/i3/src/bags.lua @@ -0,0 +1,173 @@ +local set_fs = i3.set_fs + +IMPORT("get_bag_description", "ItemStack") +IMPORT("S", "ES", "fmt", "msg", "slz", "dslz") +IMPORT("get_group", "play_sound", "get_detached_inv", "create_inventory") + +local function get_content(content) + local t = {} + + for i, v in pairs(content) do + t[i] = ItemStack(v) + end + + return t +end + +local function init_bags(player) + local name = player:get_player_name() + local data = i3.data[name] + + local bag = create_inventory(fmt("i3_bag_%s", name), { + allow_put = function(inv, _, _, stack) + local empty = inv:is_empty"main" + local item_group = get_group(stack:get_name(), "bag") + + if empty and item_group > 0 and item_group <= 4 then + return 1 + end + + if not empty then + msg(name, S"There is already a bag") + else + msg(name, S"This is not a bag") + end + + return 0, play_sound(name, "i3_cannot", 0.8) + end, + + on_put = function(_, _, _, stack) + data.bag = stack:to_string() + + local meta = stack:get_meta() + local content = dslz(meta:get_string"content") + + if content then + local inv = get_detached_inv("bag_content", name) + inv:set_list("main", get_content(content)) + end + + set_fs(player) + end, + + on_take = function() + data.bag = nil + data.bag_rename = nil + + local content = get_detached_inv("bag_content", name) + content:set_list("main", {}) + + set_fs(player) + end, + }, name) + + bag:set_size("main", 1) + + if data.bag then + bag:set_list("main", get_content{data.bag}) + end + + local function save_content(inv) + local bagstack = bag:get_stack("main", 1) + local meta = bagstack:get_meta() + local desc = get_bag_description(data, bagstack) + + if inv:is_empty"main" then + meta:set_string("description", desc) + meta:set_string("content", "") + else + local list = inv:get_list"main" + local t, c = {}, 0 + + for i = 1, #list do + local stack = list[i] + + if not stack:is_empty() then + c++ + t[i] = stack:to_string() + end + end + + local bag_size = get_group(bagstack:get_name(), "bag") + local percent = fmt("%d", (c * 100) / (bag_size * 4)) + + meta:set_string("description", ES("@1 (@2% full)", desc, percent)) + meta:set_string("content", slz(t)) + end + + bag:set_stack("main", 1, bagstack) + data.bag = bagstack:to_string() + + set_fs(player) + end + + local bag_content = create_inventory(fmt("i3_bag_content_%s", name), { + on_move = save_content, + on_put = save_content, + on_take = save_content, + + allow_put = function(_, _, _, stack) + local meta = stack:get_meta() + local content = dslz(meta:get_string"content") + + if content then + msg(name, "You cannot put a bag in another bag") + return 0, play_sound(name, "i3_cannot", 0.8) + end + + return stack:get_count() + end, + }, name) + + bag_content:set_size("main", 4*4) + + if data.bag then + local meta = bag:get_stack("main", 1):get_meta() + local content = dslz(meta:get_string"content") + + if content then + bag_content:set_list("main", get_content(content)) + end + end +end + +local bag_recipes = { + small = { + rcp = { + {"", "farming:string", ""}, + {"group:wool", "group:wool", "group:wool"}, + {"group:wool", "group:wool", "group:wool"}, + }, + size = 2, + }, + medium = { + rcp = { + {"farming:string", "i3:bag_small", "farming:string"}, + {"farming:string", "i3:bag_small", "farming:string"}, + }, + size = 3, + }, + large = { + rcp = { + {"farming:string", "i3:bag_medium", "farming:string"}, + {"farming:string", "i3:bag_medium", "farming:string"}, + }, + size = 4, + }, +} + +for size, item in pairs(bag_recipes) do + local bagname = fmt("i3:bag_%s", size) + + core.register_craftitem(bagname, { + description = fmt("%s Backpack", size:gsub("^%l", string.upper)), + inventory_image = fmt("i3_bag_%s.png", size), + groups = {bag = item.size}, + stack_max = 1, + }) + + core.register_craft{output = bagname, recipe = item.rcp} + core.register_craft{type = "fuel", recipe = bagname, burntime = 3} +end + +return init_bags diff --git a/mods/i3/src/caches.lua b/mods/i3/src/caches.lua new file mode 100644 index 0000000..5fb9a90 --- /dev/null +++ b/mods/i3/src/caches.lua @@ -0,0 +1,349 @@ +local replacements = {fuel = {}} +local http = ... + +IMPORT("maxn", "copy", "insert", "sort", "match", "sub") +IMPORT("true_str", "is_table", "valid_item", "table_merge", "table_replace", "table_eq") +IMPORT("fmt", "reg_items", "reg_aliases", "reg_nodes", "is_cube", "get_cube", "ItemStack") +IMPORT("is_group", "extract_groups", "item_has_groups", "groups_to_items", "get_group_stereotype") + +local function get_burntime(item) + return core.get_craft_result{method = "fuel", items = {item}}.time +end + +local function cache_fuel(item) + local burntime = get_burntime(item) + if burntime > 0 then + i3.fuel_cache[item] = { + type = "fuel", + items = {item}, + burntime = burntime, + replacements = replacements.fuel[item], + } + end +end + +local function cache_groups(group, groups) + i3.groups[group] = {} + i3.groups[group].groups = groups + i3.groups[group].items = groups_to_items(groups) + + if #groups == 1 then + i3.groups[group].stereotype = get_group_stereotype(groups[1]) + end + + local items = i3.groups[group].items + if #items <= 1 then return end + + local px, lim, c = 64, 10, 0 + local sprite = "[combine:WxH" + + for _, item in ipairs(items) do + local def = reg_items[item] + local tiles = def.tiles or def.tile_images + local texture = true_str(def.inventory_image) and def.inventory_image --or tiles[1] + + if def.drawtype and is_cube(def.drawtype) then + texture = get_cube(tiles) + end + + if texture then + texture = texture:gsub("%^", "\\^"):gsub(":", "\\:") .. fmt("\\^[resize\\:%ux%u", px, px) + sprite = sprite .. fmt(":0,%u=%s", c * px, texture) + c++ + if c == lim then break end + end + end + + if c > 1 then + sprite = sprite:gsub("WxH", px .. "x" .. px * c) + i3.groups[group].sprite = sprite + i3.groups[group].count = c + end +end + +local function get_item_usages(item, recipe, added) + if is_group(item) then + local group = item:sub(7) + local group_cache = i3.groups[group] + local groups = group_cache and group_cache.groups or extract_groups(item) + + if not group_cache then + cache_groups(group, groups) + end + + for name, def in pairs(reg_items) do + if not added[name] and valid_item(def) and item_has_groups(def.groups, groups) then + local usage = copy(recipe) + table_replace(usage.items, item, name) + + i3.usages_cache[name] = i3.usages_cache[name] or {} + insert(i3.usages_cache[name], 1, usage) + + added[name] = true + end + end + elseif valid_item(reg_items[item]) then + i3.usages_cache[item] = i3.usages_cache[item] or {} + insert(i3.usages_cache[item], 1, recipe) + end +end + +local function get_usages(recipe) + local added = {} + + for _, item in pairs(recipe.items) do + item = reg_aliases[item] or item + + if not added[item] then + get_item_usages(item, recipe, added) + added[item] = true + end + end +end + +local function cache_usages(item) + local recipes = i3.recipes_cache[item] or {} + + for i = 1, #recipes do + get_usages(recipes[i]) + end + + if i3.fuel_cache[item] then + i3.usages_cache[item] = table_merge(i3.usages_cache[item] or {}, {i3.fuel_cache[item]}) + end +end + +local function drop_table(name, drop) + local count_sure = 0 + local drop_items = drop.items or {} + local max_items = drop.max_items + + for i = 1, #drop_items do + local di = drop_items[i] + local valid_rarity = di.rarity and di.rarity > 1 + + if di.rarity or not max_items or + (max_items and not di.rarity and count_sure < max_items) then + for j = 1, #di.items do + local dstack = ItemStack(di.items[j]) + local dname = dstack:get_name() + local dcount = dstack:get_count() + local empty = dstack:is_empty() + + if not empty and (dname ~= name or (dname == name and dcount > 1)) then + local rarity = valid_rarity and di.rarity + + i3.register_craft { + type = rarity and "digging_chance" or "digging", + items = {name}, + output = fmt("%s %u", dname, dcount), + rarity = rarity, + tools = di.tools, + } + end + end + end + + if not di.rarity then + count_sure++ + end + end +end + +local function cache_drops(name, drop) + if true_str(drop) then + local dstack = ItemStack(drop) + local dname = dstack:get_name() + local empty = dstack:is_empty() + + if not empty and dname ~= name then + i3.register_craft { + type = "digging", + items = {name}, + output = drop, + } + end + elseif is_table(drop) then + drop_table(name, drop) + end +end + +local function cache_recipes(item) + local recipes = core.get_all_craft_recipes(item) + + if replacements[item] then + local _recipes = {} + + for k, v in ipairs(recipes or {}) do + _recipes[#recipes + 1 - k] = v + end + + local shift = 0 + local size_rpl = maxn(replacements[item]) + local size_rcp = #_recipes + + if size_rpl > size_rcp then + shift = size_rcp - size_rpl + end + + for k, v in pairs(replacements[item]) do + k += shift + + if _recipes[k] then + _recipes[k].replacements = v + end + end + + recipes = _recipes + end + + if recipes then + i3.recipes_cache[item] = table_merge(recipes, i3.recipes_cache[item] or {}) + end +end + +--[[ As `core.get_craft_recipe` and `core.get_all_craft_recipes` do not + return the fuel, replacements and toolrepair recipes, we have to + override `core.register_craft` and do some reverse engineering. + See engine's issues #4901, #5745 and #8920. ]] + +local old_register_craft = core.register_craft +local rcp_num = {} + +core.register_craft = function(def) + old_register_craft(def) + + if def.type == "toolrepair" then + i3.toolrepair = def.additional_wear * -100 + end + + local output = def.output or (true_str(def.recipe) and def.recipe) or nil + if not output then return end + output = {match(output, "%S+")} + + local groups + + if is_group(output[1]) then + groups = extract_groups(output[1]) + output = groups_to_items(groups) + end + + for i = 1, #output do + local item = output[i] + rcp_num[item] = (rcp_num[item] or 0) + 1 + + if def.replacements then + if def.type == "fuel" then + replacements.fuel[item] = def.replacements + else + replacements[item] = replacements[item] or {} + replacements[item][rcp_num[item]] = def.replacements + end + end + end +end + +local old_clear_craft = core.clear_craft + +core.clear_craft = function(def) + old_clear_craft(def) + + -- TODO: hide in crafting guide +end + +local function resolve_aliases(hash) + for oldname, newname in pairs(reg_aliases) do + cache_recipes(oldname) + local recipes = i3.recipes_cache[oldname] + + if recipes then + if not i3.recipes_cache[newname] then + i3.recipes_cache[newname] = {} + end + + local similar + + for i = 1, #i3.recipes_cache[oldname] do + local rcp_old = i3.recipes_cache[oldname][i] + + for j = 1, #i3.recipes_cache[newname] do + local rcp_new = copy(i3.recipes_cache[newname][j]) + rcp_new.output = oldname + + if table_eq(rcp_old, rcp_new) then + similar = true + break + end + end + + if not similar then + insert(i3.recipes_cache[newname], rcp_old) + end + end + end + + if newname ~= "" and i3.recipes_cache[oldname] and reg_items[newname] and not hash[newname] then + insert(i3.init_items, newname) + end + end +end + +local function init_recipes() + local _select, _preselect = {}, {} + + for name, def in pairs(reg_items) do + if name ~= "" and valid_item(def) then + cache_drops(name, def.drop) + cache_fuel(name) + cache_recipes(name) + + _preselect[name] = true + end + end + + for name in pairs(_preselect) do + cache_usages(name) + + insert(i3.init_items, name) + _select[name] = true + end + + resolve_aliases(_select) + sort(i3.init_items) + + if http and true_str(i3.export_url) then + local post_data = { + recipes = i3.recipes_cache, + usages = i3.usages_cache, + } + + http.fetch_async { + url = i3.export_url, + post_data = core.write_json(post_data), + } + end +end + +local function init_cubes() + for name, def in pairs(reg_nodes) do + if def then + local id = core.get_content_id(name) + local tiles = def.tiles or def.tile_images + + if is_cube(def.drawtype) then + i3.cubes[id] = get_cube(tiles) + elseif sub(def.drawtype, 1, 9) == "plantlike" or sub(def.drawtype, 1, 8) == "firelike" then + local texture = true_str(def.inventory_image) and def.inventory_image or tiles[1] + + if texture then + i3.plants[id] = texture + end + end + end + end +end + +return function() + init_recipes() + init_cubes() +end diff --git a/mods/i3/src/callbacks.lua b/mods/i3/src/callbacks.lua new file mode 100644 index 0000000..f72bae4 --- /dev/null +++ b/mods/i3/src/callbacks.lua @@ -0,0 +1,279 @@ +local http, storage = ... +local init_bags = i3.files.bags() +local fill_caches = i3.files.caches(http) +local init_hud = i3.files.hud() +local set_fs = i3.set_fs + +IMPORT("slz", "min", "insert", "copy", "ItemStack") +IMPORT("spawn_item", "reset_data", "get_detached_inv", "play_sound", "update_inv_size") + +core.register_on_player_hpchange(function(player, hpchange) + local name = player:get_player_name() + local data = i3.data[name] + if not data then return end + + local hp_max = player:get_properties().hp_max + data.hp = min(hp_max, player:get_hp() + hpchange) + + set_fs(player) +end) + +core.register_on_dieplayer(function(player) + local name = player:get_player_name() + local data = i3.data[name] + if not data then return end + + if i3.settings.drop_bag_on_die then + local bagstack = ItemStack(data.bag) + spawn_item(player, bagstack) + end + + data.bag = nil + local bag = get_detached_inv("bag", name) + local content = get_detached_inv("bag_content", name) + + bag:set_list("main", {}) + content:set_list("main", {}) + + set_fs(player) +end) + +core.register_on_chatcommand(function(name) + local player = core.get_player_by_name(name) + core.after(0, set_fs, player) +end) + +core.register_on_priv_grant(function(name, _, priv) + if priv == "creative" or priv == "all" then + local data = i3.data[name] + reset_data(data) + data.favs = {} + + local player = core.get_player_by_name(name) + core.after(0, set_fs, player) + end +end) + +core.register_on_player_inventory_action(function(player, _, _, info) + local name = player:get_player_name() + + if not core.is_creative_enabled(name) and + ((info.from_list == "main" and info.to_list == "craft") or + (info.from_list == "craft" and info.to_list == "main") or + (info.from_list == "craftresult" and info.to_list == "main")) then + set_fs(player) + end +end) + +if core.global_exists"armor" then + i3.modules.armor = true + + local group_indexes = { + {"armor_head", "i3_heavy_helmet"}, + {"armor_torso", "i3_heavy_armor"}, + {"armor_legs", "i3_heavy_leggings"}, + {"armor_feet", "i3_heavy_boots"}, + {"armor_shield", "i3_heavy_shield"}, + } + + local function check_group(def, group) + return def.groups[group] and def.groups[group] > 0 + end + + armor:register_on_equip(function(player, idx, stack) + local _, armor_inv = armor:get_valid_player(player, "3d_armor") + local def = stack:get_definition() + local name = player:get_player_name() + local data = i3.data[name] + + for i, v in ipairs(group_indexes) do + local group, sound = unpack(v) + local stackname = stack:get_name() + + if stackname:find"wood" or stackname:find"stone" or stackname:find"cactus" then + sound = sound:gsub("heavy", "light") + end + + if i == idx and check_group(def, group) then + data.armor_allow = sound + return armor:register_on_update(set_fs) + end + end + + data.armor_disallow = true + armor_inv:remove_item("armor", stack) + end) + + armor:register_on_update(function(player) + local _, armor_inv = armor:get_valid_player(player, "3d_armor") + if not armor_inv then return end + + for i = 1, 5 do + local stack = armor_inv:get_stack("armor", i) + local def = stack:get_definition() + + for j, v in ipairs(group_indexes) do + local group = v[1] + + if check_group(def, group) and i ~= j then + armor_inv:set_stack("armor", i, armor_inv:get_stack("armor", j)) + armor_inv:set_stack("armor", j, stack) + + return play_sound(player:get_player_name(), "i3_cannot", 0.8) + end + end + end + end) + + core.register_on_player_inventory_action(function(player, action, _, info) + if action ~= "take" then return end + local name = player:get_player_name() + local data = i3.data[name] + + if data.armor_disallow then + local inv = player:get_inventory() + inv:set_stack("main", info.index, info.stack) + data.armor_disallow = nil + play_sound(name, "i3_cannot", 0.8) + + elseif data.armor_allow then + play_sound(name, data.armor_allow, 0.8) + data.armor_allow = nil + end + end) +end + +if core.global_exists"skins" then + i3.modules.skins = true +end + +if core.global_exists"awards" then + i3.modules.awards = true + + core.register_on_craft(function(_, player) + set_fs(player) + end) + + core.register_on_dignode(function(_, _, player) + set_fs(player) + end) + + core.register_on_placenode(function(_, _, player) + set_fs(player) + end) + + core.register_on_chat_message(function(name) + local player = core.get_player_by_name(name) + set_fs(player) + end) +end + +local function disable_inventories() + if rawget(_G, "sfinv") then + function sfinv.set_player_inventory_formspec() return end + sfinv.enabled = false + end + + if rawget(_G, "unified_inventory") then + function unified_inventory.set_inventory_formspec() return end + end +end + +core.register_on_mods_loaded(function() + fill_caches() + disable_inventories() +end) + +local function get_lang_code(info) + return info and info.lang_code +end + +local function get_formspec_version(info) + return info and info.formspec_version or 1 +end + +local function outdated(name) + core.show_formspec(name, "i3_outdated", + ("size[6.5,1.3]image[0,0;1,1;i3_book.png]label[1,0;%s]button_exit[2.6,0.8;1,1;;OK]"):format( + "Your Minetest client is outdated.\nGet the latest version on minetest.net to play the game.")) +end + +local function init_data(player, info) + local name = player:get_player_name() + i3.data[name] = i3.data[name] or {} + local data = i3.data[name] + + for k, v in pairs(i3.default_data) do + local val = data[k] + if val == nil then + val = v + end + + data[k] = val + end + + data.player_name = name + data.filter = "" + data.pagenum = 1 + data.skin_pagenum = 1 + data.items = i3.init_items + data.items_raw = i3.init_items + data.favs = {} + data.show_setting = "home" + data.crafting_counts = {} + data.tab = 1 + data.itab = 1 + data.subcat = 1 + data.scrbar_inv = 0 + data.lang_code = get_lang_code(info) + data.fs_version = info.formspec_version + + update_inv_size(player, data) + + core.after(0, set_fs, player) +end + +local function save_data(player_name) + local _data = copy(i3.data) + + for name, v in pairs(_data) do + for dat in pairs(v) do + if not i3.saves[dat] then + _data[name][dat] = nil + + if player_name and i3.data[player_name] then + i3.data[player_name][dat] = nil -- To free up some memory + end + end + end + end + + storage:set_string("data", slz(_data)) +end + +insert(core.registered_on_joinplayers, 1, function(player) + local name = player:get_player_name() + local info = core.get_player_information and core.get_player_information(name) + + if not info or get_formspec_version(info) < i3.settings.min_fs_version then + return outdated(name) + end + + init_data(player, info) + init_bags(player) + init_hud(player) +end) + +core.register_on_leaveplayer(function(player) + local name = player:get_player_name() + save_data(name) +end) + +core.register_on_shutdown(save_data) + +local function routine() + save_data() + core.after(i3.settings.save_interval, routine) +end + +core.after(i3.settings.save_interval, routine) diff --git a/mods/i3/src/common.lua b/mods/i3/src/common.lua new file mode 100644 index 0000000..6161a34 --- /dev/null +++ b/mods/i3/src/common.lua @@ -0,0 +1,800 @@ +local vec = vector.new +local ItemStack = ItemStack +local loadstring = loadstring +local reg_items = core.registered_items +local translate = core.get_translated_string +local sort, concat, insert = table.sort, table.concat, table.insert +local min, floor, ceil = math.min, math.floor, math.ceil +local fmt, find, match, gmatch, sub, split, lower, upper = + string.format, string.find, string.match, string.gmatch, + string.sub, string.split, string.lower, string.upper + +if not core.registered_privileges.creative then + core.register_privilege("creative", { + description = "Allow player to use creative inventory", + give_to_singleplayer = false, + give_to_admin = false, + }) +end + +local old_is_creative_enabled = core.is_creative_enabled + +function core.is_creative_enabled(name) + if name == "" then + return old_is_creative_enabled(name) + end + + return core.check_player_privs(name, {creative = true}) or old_is_creative_enabled(name) +end + +local S = core.get_translator"i3" +local ES = function(...) return core.formspec_escape(S(...)) end + +local function is_num(x) + return type(x) == "number" +end + +local function is_str(x) + return type(x) == "string" +end + +local function is_table(x) + return type(x) == "table" +end + +local function is_func(x) + return type(x) == "function" +end + +local function true_str(str) + return is_str(str) and str ~= "" +end + +local function true_table(x) + return is_table(x) and next(x) +end + +local function reset_compression(data) + data.alt_items = nil + data.expand = "" +end + +local function msg(name, str) + local prefix = "[i3]" + return core.chat_send_player(name, fmt("%s %s", core.colorize("#ff0", prefix), str)) +end + +local function err(str) + return core.log("error", str) +end + +local function round(num, decimal) + local mul = 10 ^ decimal + return floor(num * mul + 0.5) / mul +end + +local function toupper(str) + return str:gsub("%f[%w]%l", upper):gsub("_", " ") +end + +local function utf8_len(str) + local c = 0 + + for _ in str:gmatch"[%z\1-\127\194-\244][\128-\191]*" do -- Arguably working duct-tape code + c++ + end + + return c +end + +local function get_bag_description(data, stack) + local desc = translate(data.lang_code, stack:get_description()) + desc = split(desc, "(")[1] or desc + desc = toupper(desc:trim()) + + return desc +end + +local function search(data) + reset_compression(data) + + local filter = data.filter + local opt = "^(.-)%+([%w_]+)=([%w_,]+)" + local search_filter = next(i3.search_filters) and match(filter, opt) + local filters = {} + + if search_filter then + search_filter = search_filter:trim() + + for filter_name, values in gmatch(filter, sub(opt, 6)) do + if i3.search_filters[filter_name] then + values = split(values, ",") + filters[filter_name] = values + end + end + end + + local filtered_list, c = {}, 0 + + for i = 1, #data.items_raw do + local item = data.items_raw[i] + local def = reg_items[item] + local desc = lower(translate(data.lang_code, def.description)) or "" + local search_in = fmt("%s %s", item, desc) + local temp, j, to_add = {}, 1 + + if search_filter then + for filter_name, values in pairs(filters) do + if values then + local func = i3.search_filters[filter_name] + to_add = (j > 1 and temp[item] or j == 1) and + func(item, values) and (search_filter == "" or + find(search_in, search_filter, 1, true)) + + if to_add then + temp[item] = true + end + + j++ + end + end + else + local ok = true + + for keyword in gmatch(filter, "%S+") do + if not find(search_in, keyword, 1, true) then + ok = nil + break + end + end + + if ok then + to_add = true + end + end + + if to_add then + c++ + filtered_list[c] = item + end + end + + data.items = filtered_list +end + +local function table_replace(t, val, new) + for k, v in pairs(t) do + if v == val then + t[k] = new + end + end +end + +local function table_merge(t1, t2, hash) + t1 = t1 or {} + t2 = t2 or {} + + if hash then + for k, v in pairs(t2) do + t1[k] = v + end + else + local c = #t1 + + for i = 1, #t2 do + c++ + t1[c] = t2[i] + end + end + + return t1 +end + +local function array_diff(t1, t2) + local hash = {} + + for i = 1, #t1 do + local v = t1[i] + hash[v] = true + end + + for i = 1, #t2 do + local v = t2[i] + hash[v] = nil + end + + local diff, c = {}, 0 + + for i = 1, #t1 do + local v = t1[i] + if hash[v] then + c++ + diff[c] = v + end + end + + return diff +end + +local function table_eq(t1, t2) + local ty1, ty2 = type(t1), type(t2) + if ty1 ~= ty2 then return end + + if ty1 ~= "table" and ty2 ~= "table" then + return t1 == t2 + end + + for k, v in pairs(t1) do + local v2 = t2[k] + if v2 == nil or not table_eq(v, v2) then return end + end + + for k, v in pairs(t2) do + local v1 = t1[k] + if v1 == nil or not table_eq(v1, v) then return end + end + + return true +end + +local function clean_name(item) + if sub(item, 1, 1) == ":" or sub(item, 1, 1) == " " or sub(item, 1, 1) == "_" then + item = sub(item, 2) + end + + return item +end + +local function is_group(item) + return sub(item, 1, 6) == "group:" +end + +local function extract_groups(str) + return split(sub(str, 7), ",") +end + +local function item_has_groups(item_groups, groups) + for i = 1, #groups do + local group = groups[i] + if (item_groups[group] or 0) == 0 then return end + end + + return true +end + +local function valid_item(def) + return def and def.groups.not_in_creative_inventory ~= 1 and + def.description and def.description ~= "" +end + +local function get_group_stereotype(group) + local stereotype = i3.group_stereotypes[group] + local def = reg_items[stereotype] + + if valid_item(def) then + return stereotype + end +end + +local function groups_to_items(groups) + local names = {} + + for name, def in pairs(reg_items) do + if valid_item(def) and item_has_groups(def.groups, groups) then + insert(names, name) + end + end + + sort(names) + + return names +end + +local function is_cube(drawtype) + return drawtype == "normal" or drawtype == "liquid" or + sub(drawtype, 1, 9) == "glasslike" or + sub(drawtype, 1, 8) == "allfaces" +end + +local function get_cube(tiles) + if not true_table(tiles) then + return "i3_blank.png" + end + + local top = tiles[1] or "i3_blank.png" + if is_table(top) then + top = top.name or top.image + end + + local left = tiles[3] or top or "i3_blank.png" + if is_table(left) then + left = left.name or left.image + end + + local right = tiles[5] or left or "i3_blank.png" + if is_table(right) then + right = right.name or right.image + end + + return core.inventorycube(top, left, right) +end + +local function apply_recipe_filters(recipes, player) + for _, filter in pairs(i3.recipe_filters) do + recipes = filter(recipes, player) + end + + return recipes +end + +local function recipe_filter_set() + return next(i3.recipe_filters) +end + +local function compression_active(data) + return data.collapse and not recipe_filter_set() and data.filter == "" +end + +local function compressible(item, data) + return compression_active(data) and i3.compress_groups[item] +end + +local function is_fav(data) + for i = 1, #data.favs do + if data.favs[i] == data.query_item then + return i + end + end +end + +local function sort_by_category(data) + reset_compression(data) + local items = data.items_raw + + if data.filter ~= "" then + search(data) + items = data.items + end + + local new = {} + + for i = 1, #items do + local item = items[i] + local to_add = true + + if data.itab == 2 then + to_add = core.registered_nodes[item] + elseif data.itab == 3 then + to_add = core.registered_craftitems[item] or core.registered_tools[item] + end + + if to_add then + insert(new, item) + end + end + + data.items = new +end + +local function spawn_item(player, stack) + local dir = player:get_look_dir() + local ppos = player:get_pos() + ppos.y = ppos.y + player:get_properties().eye_height + local look_at = ppos + dir + + core.add_item(look_at, stack) +end + +local function get_recipes(player, item) + item = core.registered_aliases[item] or item + local recipes = i3.recipes_cache[item] + local usages = i3.usages_cache[item] + + if recipes then + recipes = apply_recipe_filters(recipes, player) + end + + local no_recipes = not recipes or #recipes == 0 + if no_recipes and not usages then return end + usages = apply_recipe_filters(usages, player) + local no_usages = not usages or #usages == 0 + + return not no_recipes and recipes or nil, + not no_usages and usages or nil +end + +local function get_stack(player, stack) + local inv = player:get_inventory() + + if inv:room_for_item("main", stack) then + inv:add_item("main", stack) + else + spawn_item(player, stack) + end +end + +local function craft_stack(player, data, craft_rcp) + local inv = player:get_inventory() + local rcp_usg = craft_rcp and "recipe" or "usage" + local rcp_def = rcp_usg == "recipe" and data.recipes[data.rnum] or data.usages[data.unum] + local output = craft_rcp and data.recipes[data.rnum].output or data.usages[data.unum].output + output = ItemStack(output) + local stackname, stackcount, stackmax = output:get_name(), output:get_count(), output:get_stack_max() + local scrbar_val = data[fmt("scrbar_%s", craft_rcp and "rcp" or "usg")] or 1 + + for name, count in pairs(data.crafting_counts[rcp_usg].rcp) do + local items = {[name] = count} + + if is_group(name) then + items = {} + local groups = extract_groups(name) + local groupname = name:sub(7) + local item_groups = i3.groups[groupname].items or groups_to_items(groups) + local remaining = count + + for _, item in ipairs(item_groups) do + for _name, _count in pairs(data.crafting_counts[rcp_usg].inv) do + if item == _name and remaining > 0 then + local c = min(remaining, _count) + items[item] = c + remaining -= c + end + + if remaining == 0 then break end + end + end + end + + for k, v in pairs(items) do + inv:remove_item("main", fmt("%s %s", k, v * scrbar_val)) + end + end + + if rcp_def.replacements then + for _, pair in ipairs(rcp_def.replacements) do + get_stack(player, ItemStack(pair[2])) + end + end + + local count = stackcount * scrbar_val + local iter = ceil(count / stackmax) + local leftover = count + + for _ = 1, iter do + local c = min(stackmax, leftover) + local stack = ItemStack(fmt("%s %s", stackname, c)) + get_stack(player, stack) + leftover -= stackmax + end +end + +local function play_sound(name, sound, volume) + core.sound_play(sound, {to_player = name, gain = volume}, true) +end + +local function safe_teleport(player, pos) + local name = player:get_player_name() + play_sound(name, "i3_teleport", 0.8) + + local vel = player:get_velocity() + player:add_velocity(-vel) + + local p = vec(pos) + p.y += 0.25 + + player:set_pos(p) +end + +local function sorter(inv, data, mode) + sort(inv, function(a, b) + if mode == 1 then + a = translate(data.lang_code, a:get_short_description()) + b = translate(data.lang_code, b:get_short_description()) + else + a, b = a:get_count(), b:get_count() + end + + if data.reverse_sorting then + return a > b + end + + return a < b + end) +end + +local function pre_sorting(list, start_i) + local new_inv, special = {}, {} + + for i = start_i, #list do + local stack = list[i] + local empty = stack:is_empty() + local meta = stack:get_meta():to_table() + local wear = stack:get_wear() > 0 + + if not empty then + if next(meta.fields) or wear then + insert(special, stack) + else + insert(new_inv, stack) + end + end + end + + new_inv = table_merge(new_inv, special) + return new_inv +end + +local function compress_items(list, start_i) + local hash, new_inv, special = {}, {}, {} + + for i = start_i, #list do + local stack = list[i] + local name = stack:get_name() + local count = stack:get_count() + local stackmax = stack:get_stack_max() + local empty = stack:is_empty() + local meta = stack:get_meta():to_table() + local wear = stack:get_wear() > 0 + + if not empty then + if next(meta.fields) or wear or count >= stackmax then + insert(special, stack) + else + hash[name] = hash[name] or 0 + hash[name] += count + end + end + end + + for name, count in pairs(hash) do + local stackmax = ItemStack(name):get_stack_max() + local iter = ceil(count / stackmax) + local leftover = count + + for _ = 1, iter do + insert(new_inv, ItemStack(fmt("%s %u", name, min(stackmax, leftover)))) + leftover -= stackmax + end + end + + new_inv = table_merge(new_inv, special) + return new_inv +end + +local function sort_inventory(player, data) + local inv = player:get_inventory() + local list = inv:get_list"main" + local size = inv:get_size"main" + local start_i = data.ignore_hotbar and (data.hotbar_len + 1) or 1 + + if data.inv_compress then + list = compress_items(list, start_i) + else + list = pre_sorting(list, start_i) + end + + local new_inv = i3.sorting_methods[data.sort].func(list, data) + if not new_inv then return end + + if not data.ignore_hotbar then + inv:set_list("main", new_inv) + return + end + + for i = start_i, size do + local index = i - start_i + 1 + inv:set_stack("main", i, new_inv[index] or "") + end +end + +local function reset_data(data) + data.filter = "" + data.expand = "" + data.pagenum = 1 + data.rnum = 1 + data.unum = 1 + data.scrbar_rcp = 1 + data.scrbar_usg = 1 + data.query_item = nil + data.enable_search = nil + data.goto_page = nil + data.recipes = nil + data.usages = nil + data.crafting_rcp = nil + data.crafting_usg = nil + data.alt_items = nil + data.confirm_trash = nil + data.show_settings = nil + data.show_setting = "home" + data.items = data.items_raw + + if data.itab > 1 then + sort_by_category(data) + end +end + +local function add_hud_waypoint(player, name, pos, color) + return player:hud_add { + hud_elem_type = "waypoint", + name = name, + text = "m", + world_pos = pos, + number = color, + z_index = -300, + } +end + +local function get_detached_inv(name, player_name) + return core.get_inventory { + type = "detached", + name = fmt("i3_%s_%s", name, player_name) + } +end + +local function update_inv_size(player, data) + data.hotbar_len = data.legacy_inventory and 8 or 9 + data.inv_size = 4 * data.hotbar_len + + local inv = player:get_inventory() + inv:set_size("main", data.inv_size) + + player:hud_set_hotbar_itemcount(data.hotbar_len) + + core.after(0, function() + if data.legacy_inventory then + player:hud_set_hotbar_image"gui_hotbar.png" + else + player:hud_set_hotbar_image"i3_hotbar.png" + end + end) +end + +-- Much faster implementation of `unpack` +local function createunpack(n) + local ret = {"local t = ... return "} + + for k = 2, n do + ret[2 + (k - 2) * 4] = "t[" + ret[3 + (k - 2) * 4] = k - 1 + ret[4 + (k - 2) * 4] = "]" + + if k ~= n then + ret[5 + (k - 2) * 4] = "," + end + end + + return loadstring(concat(ret)) +end + +local newunpack = createunpack(33) + +------------------------------------------------------------------------------- + +local _ = { + -- Groups + is_group = is_group, + extract_groups = extract_groups, + item_has_groups = item_has_groups, + groups_to_items = groups_to_items, + get_group_stereotype = get_group_stereotype, + + -- Compression + compressible = compressible, + compression_active = compression_active, + + -- Sorting + search = search, + sorter = sorter, + get_recipes = get_recipes, + sort_inventory = sort_inventory, + sort_by_category = sort_by_category, + recipe_filter_set = recipe_filter_set, + apply_recipe_filters = apply_recipe_filters, + + -- Type checks + is_fav = is_fav, + is_str = is_str, + is_num = is_num, + is_func = is_func, + true_str = true_str, + true_table = true_table, + + -- Console + err = err, + msg = msg, + + -- Misc. functions + is_cube = is_cube, + get_cube = get_cube, + ItemStack = ItemStack, + valid_item = valid_item, + spawn_item = spawn_item, + clean_name = clean_name, + play_sound = play_sound, + reset_data = reset_data, + safe_teleport = safe_teleport, + add_hud_waypoint = add_hud_waypoint, + + -- Core functions + clr = core.colorize, + slz = core.serialize, + dslz = core.deserialize, + ESC = core.formspec_escape, + draw_cube = core.inventorycube, + get_group = core.get_item_group, + pos_to_str = core.pos_to_string, + str_to_pos = core.string_to_pos, + check_privs = core.check_player_privs, + get_player_by_name = core.get_player_by_name, + get_connected_players = core.get_connected_players, + + -- Inventory + get_stack = get_stack, + craft_stack = craft_stack, + update_inv_size = update_inv_size, + get_detached_inv = get_detached_inv, + get_bag_description = get_bag_description, + create_inventory = core.create_detached_inventory, + + -- Registered items + reg_items = core.registered_items, + reg_nodes = core.registered_nodes, + reg_tools = core.registered_tools, + reg_aliases = core.registered_aliases, + reg_entities = core.registered_entities, + reg_craftitems = core.registered_craftitems, + + -- i18n + S = S, + ES = ES, + translate = core.get_translated_string, + + -- String + sub = string.sub, + find = string.find, + fmt = string.format, + upper = string.upper, + lower = string.lower, + split = string.split, + match = string.match, + gmatch = string.gmatch, + toupper = toupper, + utf8_len = utf8_len, + + -- Table + maxn = table.maxn, + sort = table.sort, + copy = table.copy, + concat = table.concat, + insert = table.insert, + remove = table.remove, + indexof = table.indexof, + unpack = newunpack, + is_table = is_table, + table_merge = table_merge, + table_replace = table_replace, + table_eq = table_eq, + array_diff = array_diff, + + -- Math + round = round, + min = math.min, + max = math.max, + ceil = math.ceil, + floor = math.floor, + random = math.random, + + -- Vectors + vec = vector.new, + vec_round = vector.round, +} + +function i3.get(...) + local t = {} + + for i, var in ipairs{...} do + t[i] = _[var] + end + + return newunpack(t) +end diff --git a/mods/i3/src/compression.lua b/mods/i3/src/compression.lua new file mode 100644 index 0000000..fd06624 --- /dev/null +++ b/mods/i3/src/compression.lua @@ -0,0 +1,313 @@ +IMPORT("fmt", "copy", "insert") + +local wood_types = { + "acacia_wood", "aspen_wood", "junglewood", "pine_wood", +} + +local material_tools = { + "bronze", "diamond", "mese", "stone", "wood", +} + +local material_stairs = { + "acacia_wood", "aspen_wood", "brick", "bronzeblock", "cobble", "copperblock", + "desert_cobble", "desert_sandstone", "desert_sandstone_block", "desert_sandstone_brick", + "desert_stone", "desert_stone_block", "desert_stonebrick", + "glass", "goldblock", "ice", "junglewood", "mossycobble", "obsidian", + "obsidian_block", "obsidian_glass", "obsidianbrick", "pine_wood", + "sandstone", "sandstone_block", "sandstonebrick", + "silver_sandstone", "silver_sandstone_block", "silver_sandstone_brick", + "snowblock", "steelblock", "stone", "stone_block", "stonebrick", + "straw", "tinblock", +} + +local colors = { + "black", "blue", "brown", "cyan", "dark_green", "dark_grey", "green", + "grey", "magenta", "orange", "pink", "red", "violet", "yellow", +} + +local to_compress = { + ["default:wood"] = { + replace = "wood", + by = wood_types, + }, + + ["default:fence_wood"] = { + replace = "wood", + by = wood_types, + }, + + ["default:fence_rail_wood"] = { + replace = "wood", + by = wood_types, + }, + + ["default:mese_post_light"] = { + replace = "mese_post_light", + by = { + "mese_post_light_acacia_wood", + "mese_post_light_aspen_wood", + "mese_post_light_junglewood", + "mese_post_light_pine_wood", + } + }, + + ["doors:gate_wood_closed"] = { + replace = "wood", + by = wood_types, + }, + + ["wool:white"] = { + replace = "white", + by = colors + }, + + ["dye:white"] = { + replace = "white", + by = colors + }, + + ["default:axe_steel"] = { + replace = "steel", + by = material_tools + }, + + ["default:pick_steel"] = { + replace = "steel", + by = material_tools + }, + + ["default:shovel_steel"] = { + replace = "steel", + by = material_tools + }, + + ["default:sword_steel"] = { + replace = "steel", + by = material_tools + }, + + ["farming:hoe_steel"] = { + replace = "steel", + by = {"wood", "stone"} + }, + + ["stairs:slab_wood"] = { + replace = "wood", + by = material_stairs + }, + + ["stairs:stair_wood"] = { + replace = "wood", + by = material_stairs + }, + + ["stairs:stair_inner_wood"] = { + replace = "wood", + by = material_stairs + }, + + ["stairs:stair_outer_wood"] = { + replace = "wood", + by = material_stairs + }, + + ["walls:cobble"] = { + replace = "cobble", + by = {"desertcobble", "mossycobble"} + }, +} + +local circular_saw_names = { + {"micro", "_1"}, + {"panel", "_1"}, + {"micro", "_2"}, + {"panel", "_2"}, + {"micro", "_4"}, + {"panel", "_4"}, + {"micro", ""}, + {"panel", ""}, + + {"micro", "_12"}, + {"panel", "_12"}, + {"micro", "_14"}, + {"panel", "_14"}, + {"micro", "_15"}, + {"panel", "_15"}, + {"stair", "_outer"}, + {"stair", ""}, + + {"stair", "_inner"}, + {"slab", "_1"}, + {"slab", "_2"}, + {"slab", "_quarter"}, + {"slab", ""}, + {"slab", "_three_quarter"}, + {"slab", "_14"}, + {"slab", "_15"}, + + {"slab", "_two_sides"}, + {"slab", "_three_sides"}, + {"slab", "_three_sides_u"}, + {"stair", "_half"}, + {"stair", "_alt_1"}, + {"stair", "_alt_2"}, + {"stair", "_alt_4"}, + {"stair", "_alt"}, + {"stair", "_right_half"}, + + {"slope", ""}, + {"slope", "_half"}, + {"slope", "_half_raised"}, + {"slope", "_inner"}, + {"slope", "_inner_half"}, + {"slope", "_inner_half_raised"}, + {"slope", "_inner_cut"}, + {"slope", "_inner_cut_half"}, + + {"slope", "_inner_cut_half_raised"}, + {"slope", "_outer"}, + {"slope", "_outer_half"}, + {"slope", "_outer_half_raised"}, + {"slope", "_outer_cut"}, + {"slope", "_outer_cut_half"}, + {"slope", "_outer_cut_half_raised"}, + {"slope", "_cut"}, +} + +local moreblocks_nodes = { + "coal_stone", + "wood_tile", + "iron_checker", + "circle_stone_bricks", + "cobble_compressed", + "plankstone", + "clean_glass", + "split_stone_tile", + "all_faces_tree", + "dirt_compressed", + "coal_checker", + "clean_glow_glass", + "tar", + "clean_super_glow_glass", + "stone_tile", + "cactus_brick", + "super_glow_glass", + "desert_cobble_compressed", + "copperpatina", + "coal_stone_bricks", + "glow_glass", + "cactus_checker", + "all_faces_pine_tree", + "all_faces_aspen_tree", + "all_faces_acacia_tree", + "all_faces_jungle_tree", + "iron_stone", + "grey_bricks", + "wood_tile_left", + "wood_tile_down", + "wood_tile_center", + "wood_tile_right", + "wood_tile_full", + "checker_stone_tile", + "iron_glass", + "iron_stone_bricks", + "wood_tile_flipped", + "wood_tile_offset", + "coal_glass", + + "straw", + + "stone", + "stone_block", + "cobble", + "mossycobble", + "brick", + "sandstone", + "steelblock", + "goldblock", + "copperblock", + "bronzeblock", + "diamondblock", + "tinblock", + "desert_stone", + "desert_stone_block", + "desert_cobble", + "meselamp", + "glass", + "tree", + "wood", + "jungletree", + "junglewood", + "pine_tree", + "pine_wood", + "acacia_tree", + "acacia_wood", + "aspen_tree", + "aspen_wood", + "obsidian", + "obsidian_block", + "obsidianbrick", + "obsidian_glass", + "stonebrick", + "desert_stonebrick", + "sandstonebrick", + "silver_sandstone", + "silver_sandstone_brick", + "silver_sandstone_block", + "desert_sandstone", + "desert_sandstone_brick", + "desert_sandstone_block", + "sandstone_block", + "coral_skeleton", + "ice", +} + +local colors_moreblocks = copy(colors) +insert(colors_moreblocks, "white") + +local moreblocks_mods = { + wool = colors_moreblocks, + moreblocks = moreblocks_nodes, +} + +local t = {} + +for mod, v in pairs(moreblocks_mods) do +for _, nodename in ipairs(v) do + t[nodename] = {} + + for _, shape in ipairs(circular_saw_names) do + if shape[1] ~= "slope" or shape[2] ~= "" then + insert(t[nodename], fmt("%s_%s%s", shape[1], nodename, shape[2])) + end + end + + local slope_name = fmt("slope_%s", nodename) + + to_compress[fmt("%s:%s", mod, slope_name)] = { + replace = slope_name, + by = t[nodename], + } +end +end + +local compressed = {} + +for k, v in pairs(to_compress) do + compressed[k] = compressed[k] or {} + + for _, str in ipairs(v.by) do + local it = k:gsub(v.replace, str) + insert(compressed[k], it) + end +end + +local _compressed = {} + +for _, v in pairs(compressed) do +for _, v2 in ipairs(v) do + _compressed[v2] = true +end +end + +i3.compress_groups, i3.compressed = compressed, _compressed diff --git a/mods/i3/src/detached_inv.lua b/mods/i3/src/detached_inv.lua new file mode 100644 index 0000000..9fd750d --- /dev/null +++ b/mods/i3/src/detached_inv.lua @@ -0,0 +1,24 @@ +local set_fs = i3.set_fs +IMPORT("play_sound", "create_inventory") + +local trash = create_inventory("i3_trash", { + allow_put = function(_, _, _, stack) + return stack:get_count() + end, + + on_put = function(inv, listname, _, _, player) + inv:set_list(listname, {}) + + local name = player:get_player_name() + local data = i3.data[name] + data.armor_allow = nil + + play_sound(name, "i3_trash", 1.0) + + if not core.is_creative_enabled(name) then + set_fs(player) + end + end, +}) + +trash:set_size("main", 1) diff --git a/mods/i3/src/fields.lua b/mods/i3/src/fields.lua new file mode 100644 index 0000000..e21441d --- /dev/null +++ b/mods/i3/src/fields.lua @@ -0,0 +1,475 @@ +local set_fs = i3.set_fs + +IMPORT("min", "max", "vec_round") +IMPORT("reg_items", "reg_aliases") +IMPORT("sort", "copy", "insert", "remove", "indexof") +IMPORT("S", "random", "translate", "compressible", "ItemStack") +IMPORT("fmt", "find", "match", "sub", "lower", "split", "toupper") +IMPORT("valid_item", "get_stack", "craft_stack", "clean_name", "check_privs", "safe_teleport") +IMPORT("msg", "is_fav", "pos_to_str", "str_to_pos", "add_hud_waypoint", "play_sound", "reset_data") +IMPORT("search", "sort_inventory", "sort_by_category", "get_recipes", "get_detached_inv", "update_inv_size") + +local function inv_fields(player, data, fields) + local name = data.player_name + local inv = player:get_inventory() + local sb_inv = fields.scrbar_inv + + if sb_inv and sub(sb_inv, 1, 3) == "CHG" then + data.scrbar_inv = tonumber(match(sb_inv, "%d+")) + return + end + + if fields.dd_sorting_method then + data.sort = tonumber(fields.dd_sorting_method) + elseif fields.sb_font_size then + data.font_size = tonumber(fields.sb_font_size:match"-?%d+$") + end + + for field in pairs(fields) do + if sub(field, 1, 4) == "btn_" then + data.subcat = indexof(i3.categories, sub(field, 5)) + break + + elseif sub(field, 1, 3) == "cb_" then + local str = sub(field, 4) + data[str] = false + + if fields[field] == "true" then + data[str] = true + end + + if str == "legacy_inventory" then + update_inv_size(player, data) + elseif str == "collapse" then + search(data) + end + + elseif sub(field, 1, 8) == "setting_" then + data.show_setting = match(field, "_(%w+)$") + + elseif sub(field, 1, 9) == "skin_btn_" then + local id = tonumber(field:match("%d+")) + local _skins = skins.get_skinlist_for_player(name) + + play_sound(name, "i3_skin_change", 0.6) + skins.set_player_skin(player, _skins[id]) + + elseif find(field, "waypoint_%d+") then + local id, action = match(field, "_(%d+)_(%w+)$") + id = tonumber(id) + local waypoint = data.waypoints[id] + if not waypoint then return end + + if action == "see" then + if data.waypoint_see and data.waypoint_see == id then + data.waypoint_see = nil + else + data.waypoint_see = id + end + + elseif action == "delete" then + player:hud_remove(waypoint.id) + remove(data.waypoints, id) + + elseif action == "teleport" then + local pos = str_to_pos(waypoint.pos) + safe_teleport(player, pos) + msg(name, S("Teleported to: @1", waypoint.name)) + + elseif action == "refresh" then + local color = random(0xffffff) + waypoint.color = color + player:hud_change(waypoint.id, "number", color) + + elseif action == "hide" then + if waypoint.hide then + local new_id = add_hud_waypoint( + player, waypoint.name, str_to_pos(waypoint.pos), waypoint.color) + + waypoint.id = new_id + waypoint.hide = nil + else + player:hud_remove(waypoint.id) + waypoint.hide = true + end + end + + break + end + end + + if fields.quit then + data.confirm_trash = nil + data.show_settings = nil + data.waypoint_see = nil + data.bag_rename = nil + data.goto_page = nil + + if data.filter == "" then + data.enable_search = nil + end + + elseif fields.trash then + data.show_settings = nil + data.confirm_trash = true + + elseif fields.settings then + if not data.show_settings then + data.confirm_trash = nil + data.show_settings = true + else + data.show_settings = nil + end + + elseif fields.confirm_trash_yes or fields.confirm_trash_no then + if fields.confirm_trash_yes then + inv:set_list("main", {}) + inv:set_list("craft", {}) + end + + data.confirm_trash = nil + + elseif fields.close_settings then + data.show_settings = nil + + elseif fields.close_preview then + data.waypoint_see = nil + + elseif fields.sort then + sort_inventory(player, data) + + elseif fields.home then + if not data.home then + return msg(name, "No home set") + elseif not check_privs(name, {home = true}) then + return msg(name, "'home' privilege missing") + end + + safe_teleport(player, str_to_pos(data.home)) + msg(name, S"Welcome back home!") + + elseif fields.set_home then + data.home = pos_to_str(player:get_pos(), 1) + + elseif fields.bag_rename then + data.bag_rename = true + + elseif fields.confirm_rename then + local bag = get_detached_inv("bag", name) + local bagstack = bag:get_stack("main", 1) + local meta = bagstack:get_meta() + local desc = translate(data.lang_code, bagstack:get_description()) + local fill = split(desc, "(")[2] + local newname = fields.bag_newname:gsub("([%(%)])", "") + newname = toupper(newname:trim()) + + if fill then + newname = fmt("%s (%s", newname, fill) + end + + meta:set_string("description", newname) + bag:set_stack("main", 1, bagstack) + + data.bag = bagstack:to_string() + data.bag_rename = nil + + elseif fields.waypoint_add then + local max_waypoints = i3.settings.max_waypoints + + if #data.waypoints >= max_waypoints then + play_sound(name, "i3_cannot", 0.8) + return msg(name, fmt("Waypoints limit reached (%u)", max_waypoints)) + end + + local pos = player:get_pos() + + for _, v in ipairs(data.waypoints) do + if vec_round(pos) == vec_round(str_to_pos(v.pos)) then + play_sound(name, "i3_cannot", 0.8) + return msg(name, S"You already have set a waypoint at this position") + end + end + + local waypoint = fields.waypoint_name + + if fields.waypoint_name == "" then + waypoint = "Waypoint" + end + + local color = random(0xffffff) + local id = add_hud_waypoint(player, waypoint, pos, color) + + insert(data.waypoints, { + name = waypoint, + pos = pos_to_str(pos, 1), + color = color, + id = id, + }) + + data.scrbar_inv += 1000 + + elseif fields.hide_debug_grid then + data.hide_debug_grid = not data.hide_debug_grid + end + + return set_fs(player) +end + +local function select_item(player, data, fields) + local item + + for field in pairs(fields) do + if find(field, ":") then + item = field + break + end + end + + if not item then return end + + if compressible(item, data) then + local idx + + for i = 1, #data.items do + local it = data.items[i] + if it == item then + idx = i + break + end + end + + if data.expand ~= "" then + data.alt_items = nil + + if item == data.expand then + data.expand = nil + return + end + end + + if idx and item ~= data.expand then + data.alt_items = copy(data.items) + data.expand = item + + if i3.compress_groups[item] then + local items = copy(i3.compress_groups[item]) + insert(items, fmt("_%s", item)) + + sort(items, function(a, b) + if a:sub(1, 1) == "_" then + a = a:sub(2) + end + + return a < b + end) + + local i = 1 + + for _, v in ipairs(items) do + if valid_item(reg_items[clean_name(v)]) then + insert(data.alt_items, idx + i, v) + i++ + end + end + end + end + else + if sub(item, 1, 1) == "_" then + item = sub(item, 2) + elseif sub(item, 1, 6) == "group!" then + item = match(item, "([%w:_]+)$") + end + + item = reg_aliases[item] or item + if not reg_items[item] then return end + + if core.is_creative_enabled(data.player_name) then + local stack = ItemStack(item) + local stackmax = stack:get_stack_max() + stack = fmt("%s %s", item, stackmax) + + return get_stack(player, stack) + end + + if item == data.query_item then return end + local recipes, usages = get_recipes(player, item) + + data.query_item = item + data.recipes = recipes + data.usages = usages + data.rnum = 1 + data.unum = 1 + data.scrbar_rcp = 1 + data.scrbar_usg = 1 + data.crafting_rcp = nil + data.crafting_usg = nil + end +end + +local function rcp_fields(player, data, fields) + local sb_rcp, sb_usg = fields.scrbar_rcp, fields.scrbar_usg + + if not data.hide_tabs and fields.filter and fields.filter == "" then + data.enable_search = nil + end + + if fields.cancel then + reset_data(data) + + elseif fields.exit then + data.query_item = nil + + elseif fields.enable_search then + if data.hide_tabs then + data.enable_search = not data.enable_search + else + data.enable_search = true + end + + elseif fields.filter and (fields.key_enter_field == "filter" or fields.search) then + if fields.filter == "" then + reset_data(data) + return set_fs(player) + end + + local str = lower(fields.filter) + if data.filter == str then return end + + data.filter = str + data.pagenum = 1 + + search(data) + + if data.itab > 1 then + sort_by_category(data) + end + + elseif fields.pagenum then + data.goto_page = not data.goto_page + + elseif fields.goto_page then + local pagenum = tonumber(fields.goto_page) + data.pagenum = max(1, min(data.pagemax, pagenum or data.pagenum)) + data.goto_page = nil + + elseif fields.prev_page or fields.next_page then + if data.pagemax == 1 then return end + data.pagenum -= (fields.prev_page and 1 or -1) + + if data.pagenum > data.pagemax then + data.pagenum = 1 + elseif data.pagenum == 0 then + data.pagenum = data.pagemax + end + + elseif fields.prev_skin or fields.next_skin then + if data.skin_pagemax == 1 then return end + data.skin_pagenum -= (fields.prev_skin and 1 or -1) + + if data.skin_pagenum > data.skin_pagemax then + data.skin_pagenum = 1 + elseif data.skin_pagenum == 0 then + data.skin_pagenum = data.skin_pagemax + end + + elseif fields.prev_recipe or fields.next_recipe then + local num = data.rnum + (fields.prev_recipe and -1 or 1) + data.rnum = data.recipes[num] and num or (fields.prev_recipe and #data.recipes or 1) + data.crafting_rcp = nil + data.scrbar_rcp = 1 + + elseif fields.prev_usage or fields.next_usage then + local num = data.unum + (fields.prev_usage and -1 or 1) + data.unum = data.usages[num] and num or (fields.prev_usage and #data.usages or 1) + data.crafting_usg = nil + data.scrbar_usg = 1 + + elseif fields.fav then + local fav = is_fav(data) + + if #data.favs < i3.settings.max_favs and not fav then + insert(data.favs, data.query_item) + elseif fav then + remove(data.favs, fav) + end + + elseif fields.crafting_rcp or fields.crafting_usg then + if fields.crafting_rcp then + data.crafting_rcp = not data.crafting_rcp + + if not data.crafting_rcp then + data.scrbar_rcp = 1 + end + else + data.crafting_usg = not data.crafting_usg + + if not data.crafting_usg then + data.scrbar_usg = 1 + end + end + + elseif (sb_rcp and sub(sb_rcp, 1, 3) == "CHG") or (sb_usg and sub(sb_usg, 1, 3) == "CHG") then + data.scrbar_rcp = sb_rcp and tonumber(match(sb_rcp, "%d+")) + data.scrbar_usg = sb_usg and tonumber(match(sb_usg, "%d+")) + + elseif fields.craft_rcp or fields.craft_usg then + craft_stack(player, data, fields.craft_rcp) + + if fields.craft_rcp then + data.crafting_rcp = nil + data.scrbar_rcp = 1 + else + data.crafting_usg = nil + data.scrbar_usg = 1 + end + else + select_item(player, data, fields) + end +end + +core.register_on_player_receive_fields(function(player, formname, fields) + local name = player:get_player_name() + + if formname == "i3_outdated" then + return false, core.kick_player(name, + S"Your Minetest client needs updating (www.minetest.net)") + elseif formname ~= "" then + return false + end + +-- No-op buttons + if fields.player_name or fields.awards or fields.home_pos or fields.no_item or + fields.no_rcp or fields.select_sorting or fields.sort_method or fields.bg_content or + fields.quick_crafting then + return false + end + +-- print(dump(fields)) + local data = i3.data[name] + if not data then return end + + for f in pairs(fields) do + if sub(f, 1, 4) == "tab_" then + local tabname = sub(f, 5) + i3.set_tab(player, tabname) + break + elseif sub(f, 1, 5) == "itab_" then + data.pagenum = 1 + data.itab = tonumber(f:sub(-1)) + sort_by_category(data) + break + end + end + + rcp_fields(player, data, fields) + + local tab = i3.tabs[data.tab] + + if tab and tab.fields then + return true, tab.fields(player, data, fields) + end + + return true, set_fs(player) +end) + +return inv_fields diff --git a/mods/i3/src/groups.lua b/mods/i3/src/groups.lua new file mode 100644 index 0000000..6db4cd0 --- /dev/null +++ b/mods/i3/src/groups.lua @@ -0,0 +1,62 @@ +IMPORT("S") + +i3.group_stereotypes = { + dye = "dye:white", + wool = "wool:white", + wood = "default:wood", + tree = "default:tree", + sand = "default:sand", + glass = "default:glass", + stick = "default:stick", + stone = "default:stone", + leaves = "default:leaves", + coal = "default:coal_lump", + fence = "default:fence_wood", + vessel = "vessels:glass_bottle", + flower = "flowers:dandelion_yellow", + water_bucket = "bucket:bucket_water", + mesecon_conductor_craftable = "mesecons:wire_00000000_off", +} + +i3.group_names = { + dye = S"Any dye", + coal = S"Any coal", + sand = S"Any sand", + tree = S"Any tree", + wool = S"Any wool", + glass = S"Any glass", + stick = S"Any stick", + stone = S"Any stone", + fence = S"Any fence", + carpet = S"Any carpet", + flower = S"Any flower", + leaves = S"Any leaves", + vessel = S"Any vessel", + wood = S"Any wood planks", + mushroom = S"Any mushroom", + + ["color_red,flower"] = S"Any red flower", + ["color_blue,flower"] = S"Any blue flower", + ["color_black,flower"] = S"Any black flower", + ["color_white,flower"] = S"Any white flower", + ["color_green,flower"] = S"Any green flower", + ["color_orange,flower"] = S"Any orange flower", + ["color_yellow,flower"] = S"Any yellow flower", + ["color_violet,flower"] = S"Any violet flower", + + ["color_red,dye"] = S"Any red dye", + ["color_blue,dye"] = S"Any blue dye", + ["color_grey,dye"] = S"Any grey dye", + ["color_pink,dye"] = S"Any pink dye", + ["color_cyan,dye"] = S"Any cyan dye", + ["color_black,dye"] = S"Any black dye", + ["color_white,dye"] = S"Any white dye", + ["color_brown,dye"] = S"Any brown dye", + ["color_green,dye"] = S"Any green dye", + ["color_orange,dye"] = S"Any orange dye", + ["color_yellow,dye"] = S"Any yellow dye", + ["color_violet,dye"] = S"Any violet dye", + ["color_magenta,dye"] = S"Any magenta dye", + ["color_dark_grey,dye"] = S"Any dark grey dye", + ["color_dark_green,dye"] = S"Any dark green dye", +} diff --git a/mods/i3/src/gui.lua b/mods/i3/src/gui.lua new file mode 100644 index 0000000..365e6c6 --- /dev/null +++ b/mods/i3/src/gui.lua @@ -0,0 +1,1742 @@ +local damage_enabled = i3.settings.damage_enabled +local debug_mode = i3.settings.debug_mode + +local model_aliases = i3.files.model_alias() +local PNG, styles, fs_elements, colors = i3.files.styles() + +local sprintf = string.format +local VoxelArea, VoxelManip = VoxelArea, VoxelManip + +IMPORT("vec", "vec_round") +IMPORT("find", "match", "sub", "upper") +IMPORT("clr", "ESC", "msg", "check_privs") +IMPORT("min", "max", "floor", "ceil", "round") +IMPORT("true_str", "is_fav", "is_num", "str_to_pos") +IMPORT("reg_items", "reg_nodes", "reg_tools", "reg_entities") +IMPORT("get_bag_description", "get_detached_inv", "get_recipes") +IMPORT("compression_active", "compressible", "recipe_filter_set") +IMPORT("S", "ES", "translate", "ItemStack", "toupper", "utf8_len") +IMPORT("maxn", "sort", "concat", "copy", "insert", "remove", "unpack") +IMPORT("extract_groups", "groups_to_items", "is_group", "item_has_groups", "get_group") + +local function fmt(elem, ...) + if not fs_elements[elem] then + return sprintf(elem, ...) + end + + return sprintf(fs_elements[elem], ...) +end + +local function repairable(tool) + local def = reg_tools[tool] + return i3.toolrepair and def and def.groups and def.groups.disable_repair ~= 1 +end + +local function weird_desc(str) + return not true_str(str) or find(str, "\n") or not find(str, "%u") +end + +local function snip(str, limit, font_size) + limit -= (font_size > 3 and font_size + 1 or font_size) + + if utf8_len(str) > limit then + return fmt("%s...", sub(str, 1, limit - 3)) + end +end + +local function get_desc(item, lang_code) + if sub(item, 1, 1) == "_" then + item = sub(item, 2) + end + + local def = reg_items[item] + if not def then + return S("Unknown Item (@1)", item) + end + + local desc = ItemStack(item):get_short_description() + desc = translate(lang_code, desc) + + if true_str(desc) then + desc = desc:trim():gsub("_", " ") + + if not find(desc, "%u") then + desc = toupper(desc) + end + + return desc + + elseif true_str(item) then + return toupper(match(item, ":(.*)")) + end +end + +local function get_stack_max(inv, data, is_recipe, rcp) + local list = inv:get_list"main" + local size = inv:get_size"main" + local counts_inv, counts_rcp, counts = {}, {}, {} + local rcp_usg = is_recipe and "recipe" or "usage" + + for _, it in pairs(rcp.items) do + counts_rcp[it] = (counts_rcp[it] or 0) + 1 + end + + data.crafting_counts[rcp_usg] = {} + data.crafting_counts[rcp_usg].rcp = counts_rcp + + for i = 1, size do + local stack = list[i] + + if not stack:is_empty() then + local item = stack:get_name() + local count = stack:get_count() + + for name in pairs(counts_rcp) do + if is_group(name) then + local def = reg_items[item] + + if def then + local group_cache = i3.groups[name:sub(7)] + local groups = group_cache and group_cache.groups or extract_groups(name) + + if item_has_groups(def.groups, groups) then + counts_inv[name] = (counts_inv[name] or 0) + count + end + end + end + end + + counts_inv[item] = (counts_inv[item] or 0) + count + end + end + + data.crafting_counts[rcp_usg].inv = counts_inv + + for name in pairs(counts_rcp) do + counts[name] = floor((counts_inv[name] or 0) / (counts_rcp[name] or 0)) + end + + local max_stacks = math.huge + + for _, count in pairs(counts) do + if count < max_stacks then + max_stacks = count + end + end + + return max_stacks +end + +local function get_inv_slots(data, fs) + local legacy_inventory = data.legacy_inventory + local hotbar_len = data.hotbar_len + local inv_x = legacy_inventory and 0.23 or 0.22 + local inv_y = legacy_inventory and 6.7 or 6.9 + local spacing = legacy_inventory and 0.25 or 0.1 + local size = 1 + + fs"style_type[box;colors=#77777710,#77777710,#777,#777]" + + for i = 0, hotbar_len - 1 do + box(i * size + inv_x + (i * spacing), inv_y, size, size, "") + end + + fs("style_type[list;size=%f;spacing=%f]", size, spacing) + fs("list[current_player;main;%f,%f;%u,1;]", inv_x, inv_y, hotbar_len) + + fs("style_type[list;size=%f;spacing=%f,%f]", size, spacing, legacy_inventory and 0.15 or spacing) + + fs("list[current_player;main;%f,%f;%u,%u;%u]", inv_x, inv_y + (legacy_inventory and 1.25 or 1.15), + hotbar_len, data.inv_size / hotbar_len, hotbar_len) + + fs"style_type[list;size=1;spacing=0.15]" + fs"listring[current_player;craft]listring[current_player;main]" +end + +local function add_subtitle(fs, name, y, ctn_len, font_size, sep, label) + fs("style[%s;font=bold;font_size=%u]", name, font_size) + button(0, y, ctn_len, 0.5, name, ESC(label)) + + if sep then + image(0, y + 0.55, ctn_len, 0.035, PNG.bar) + end +end + +local function get_award_list(data, fs, ctn_len, yextra, award_list, awards_unlocked, award_list_nb) + local percent = fmt("%.1f%%", (awards_unlocked * 100) / award_list_nb):gsub("%.0", "") + + add_subtitle(fs, "awards", yextra, ctn_len, 18, false, + ES("Achievements: @1 of @2 (@3)", awards_unlocked, award_list_nb, percent)) + + for i = 1, award_list_nb do + local award = award_list[i] + local y = yextra - 0.6 + i + (i * 0.3) + + local def, progress = award.def, award.progress + local title, desc = def.title, def.description + + title = translate(data.lang_code, title) + desc = translate(data.lang_code, desc):gsub("%.$", "") + + local title_lim, desc_lim = 27, 39 + local icon_size = 1.1 + + local _title = snip(title, title_lim, data.font_size) or title + local _desc = snip(desc, desc_lim, data.font_size) or desc + + if not award.unlocked and def.secret then + title = ES"Secret award" + desc = ES"Unlock this award to find out what it is" + end + + local icon = def.icon or "awards_unknown.png" + + if not award.unlocked then + icon = fmt("%s^\\[colorize:#000:220", icon) + end + + insert(fs, fmt("image", 0, y, icon_size, icon_size, icon)) + insert(fs, "style_type[box;colors=#bababa30,#bababa30,#bababa05,#bababa05]") + insert(fs, fmt("box", 0, y, ctn_len, icon_size, "")) + + if progress then + local box_len = ctn_len - icon_size - 0.15 + local current, target = progress.current, progress.target + local curr_bar = (current * box_len) / target + + insert(fs, fmt("box", icon_size + 0.15, y + 0.8, box_len, 0.3, "#101010")) + insert(fs, "style_type[box;colors=#9dc34c80,#9dc34c,#9dc34c,#9dc34c80]") + insert(fs, fmt("box", icon_size + 0.15, y + 0.8, curr_bar, 0.3, "")) + insert(fs, "style_type[label;font_size=14]") + insert(fs, fmt("label", icon_size + 0.55, y + 0.97, fmt("%u / %u", current, target))) + + y -= 0.14 + end + + local end_title = ESC(_title or title) + local end_desc = ESC(_desc or desc) + + insert(fs, "style_type[label;font=bold;font_size=17]") + insert(fs, fmt("label", icon_size + 0.2, y + 0.4, end_title)) + insert(fs, "style_type[label;font=normal;font_size=15]") + insert(fs, fmt("label", icon_size + 0.2, y + 0.75, clr("#bbb", end_desc))) + insert(fs, "style_type[label;font_size=16]") + insert(fs, fmt("tooltip", 0, y, icon_size, icon_size, ESC(desc))) + end +end + +local function get_isometric_view(fs, pos, X, Y, t, cubes, depth, high) + pos = vec_round(pos) + cubes = cubes or 0 + depth = depth or -1 + high = high or math.huge + + t = t or {} + t[depth] = {} + + local width = 8 + local base_height = 4 + local base_depth = depth == -1 + local max_depth = -7 + local height = base_depth and (base_height - 1) or depth + + local pos1 = vec(pos.x - width, pos.y + depth, pos.z - width) + local pos2 = vec(pos.x + width, pos.y + height, pos.z + width) + + local vm = VoxelManip(pos1, pos2) + local emin, emax = vm:get_emerged_area() + local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax} + local data = vm:get_data() + + for idx in area:iterp(pos1, pos2) do + local cube = i3.cubes[data[idx]] + local plant = i3.plants[data[idx]] + local img = cube or plant + + if img then + local p = area:position(idx) + p -= pos + + local size = 0.25 + local x = 2 + (size / 2 * (p.z - p.x)) + local y = 1 + (size / 4 * (p.x + p.z - 2 * p.y)) + + if y < high then + high = y + end + + if plant then + size -= 0.05 + end + + cubes++ + insert(t[depth], {x + X, y + Y, size, size, img}) + end + end + + local maxc = ((width * 2) ^ 2) * base_height + + if cubes < maxc and depth > max_depth then + -- if there's not enough map to preview, go deeper + depth -= 1 + return get_isometric_view(fs, pos, X, Y, t, cubes, depth, high) + end + + local shift = -0.3 - high + + for i = max_depth, 0 do + local dth = t[i] + if dth then + dth[0] = #dth + for j = 1, dth[0] do + local params = dth[j] + params[2] += shift + insert(fs, fmt("image[%f,%f;%.1f,%.1f;%s]", unpack(params))) + end + end + end + + shift += (base_depth and 0.45 or 0.95) + animated_image(2.75, Y + shift, 3/14, 0.3, "i3_flag_anim.png", 4, 150) +end + +local function get_waypoint_fs(fs, data, player, yextra, ctn_len) + fs("box[0,%f;4.9,0.6;#bababa25]", yextra + 1.1) + label(0, yextra + 0.85, ES"New waypoint" .. ":") + fs("field[0.1,%f;4.8,0.6;waypoint_name;;]", yextra + 1.1) + image_button(5.1, yextra + 1.15, 0.5, 0.5, "", "waypoint_add", "") + fs("tooltip[waypoint_add;%s]", ES"Add waypoint") + + if #data.waypoints == 0 then return end + fs"style_type[label;font=bold;font_size=17]" + + for i, v in ipairs(data.waypoints) do + local y = yextra + 1.35 + (i - (i * 0.3)) + local icon_size, yi = 0.35, y + 0.12 + + fs"style_type[box;colors=#bababa30,#bababa30,#bababa05,#bababa05]" + box(0, y, ctn_len, 0.6, "") + + local waypoint_name, lim = v.name, 22 + waypoint_name = snip(waypoint_name, lim, data.font_size) or waypoint_name + + local hex = fmt("%02x", v.color) + + while #hex < 6 do + hex = "0" .. hex + end + + local teleport_priv = check_privs(player, {teleport = true}) + local waypoint_preview = data.waypoint_see and data.waypoint_see == i + + label(0.15, y + 0.33, clr(fmt("#%s", hex), waypoint_name)) + + local tooltip = fmt("Name: %s\nPosition:%s", clr("#dbeeff", v.name), + v.pos:sub(2,-2):gsub("(%-*%d*%.?%d+)", clr("#dbeeff", " %1"))) + + if teleport_priv then + tooltip = fmt("%s\n%s", tooltip, clr("#ff0", ES"[Click to teleport]")) + end + + tooltip(0, y, ctn_len - 2.1, 0.65, tooltip) + + local del = fmt("waypoint_%u_delete", i) + fs("style[%s;fgimg=%s;fgimg_hovered=%s;content_offset=0]", del, PNG.trash, PNG.trash_hover) + image_button(ctn_len - 0.5, yi, icon_size, icon_size, "", del, "") + fs("tooltip[%s;%s]", del, ES"Remove waypoint") + + local rfs = fmt("waypoint_%u_refresh", i) + fs("style[%s;fgimg=%s;fgimg_hovered=%s;content_offset=0]", rfs, PNG.refresh, PNG.refresh_hover) + image_button(ctn_len - 1, yi, icon_size, icon_size, "", rfs, "") + fs("tooltip[%s;%s]", rfs, ES"Change color") + + local see = fmt("waypoint_%u_see", i) + fs("style[%s;fgimg=%s;fgimg_hovered=%s;content_offset=0]", + see, waypoint_preview and PNG.search_hover or PNG.search, PNG.search, PNG.search_hover) + image_button(ctn_len - 1.5, yi, icon_size, icon_size, "", see, "") + fs("tooltip[%s;%s]", see, ES"Preview the waypoint area") + + local vsb = fmt("waypoint_%u_hide", i) + fs("style[%s;fgimg=%s;content_offset=0]", vsb, v.hide and PNG.nonvisible or PNG.visible) + image_button(ctn_len - 2, yi, icon_size, icon_size, "", vsb, "") + fs("tooltip[%s;%s]", vsb, v.hide and ES"Show waypoint" or ES"Hide waypoint") + + if teleport_priv then + local tp = fmt("waypoint_%u_teleport", i) + button(0, y, ctn_len - 2.1, 0.6, tp, "") + end + + if waypoint_preview then + image(0.25, y - 3.5, 5, 4, PNG.bg_content) + fs"style[area_preview;font_size=16;textcolor=#ddd]" + button(0.25, y - 3.35, 5, 0.55, "area_preview", v.name) + image_button(4.65, y - 3.25, 0.25, 0.25, PNG.cancel_hover .. "^\\[brighten", "close_preview", "") + + local pos = str_to_pos(data.waypoints[i].pos) + get_isometric_view(fs, pos, 0.6, y - 2.5) + end + end + + fs"style_type[label;font=normal;font_size=16;textcolor=#fff]" +end + +local function get_bag_fs(fs, data, bag_size, yextra) + fs("list[detached:i3_bag_%s;main;0,%f;1,1;]", data.player_name, yextra + 0.7) + local bag = get_detached_inv("bag", data.player_name) + if bag:is_empty"main" then return end + + local v = {{1.9, 2, 0.12}, {3.05, 5, 0.06}, {4.2, 10}, {4.75, 10}} + local h, m, yy = unpack(v[bag_size]) + + local bagstack = bag:get_stack("main", 1) + local desc = ESC(get_bag_description(data, bagstack)) + + image(0.5, yextra + 1.85, 0.6, 0.6, PNG.arrow_content) + fs("style[bg_content;bgimg=%s;fgimg=i3_blank.png;bgimg_middle=10,%u;sound=]", PNG.bg_content, m) + image_button(1.1, yextra + 0.5 + (yy or 0), 4.75, h, "", "bg_content", "") + + if not data.bag_rename then + hypertext(1.3, yextra + 0.8, 4.3, 0.6, "content", + fmt("
%s
", desc)) + image_button(5.22, yextra + 0.835, 0.25, 0.25, "", "bag_rename", "") + fs("tooltip[bag_rename;%s]", ES"Rename the bag") + else + box(1.7, yextra + 0.82, 2.6, 0.4, "#707070") + fs("field[1.8,%f;2.5,0.4;bag_newname;;%s]", yextra + 0.82, desc) + fs"field_close_on_enter[bag_newname;false]" + hypertext(4.4, yextra + 0.88, 0.8, 0.6, "confirm_rename", + fmt("" .. + "
OK
", colors.yellow)) + end + + local x, size, spacing = 1.45, 0.9, 0.12 + + if bag_size == 4 then + x, size, spacing = 1.7, 0.8, 0.1 + end + + fs("style_type[list;size=%f;spacing=%f]", size, spacing) + fs("list[detached:i3_bag_content_%s;main;%f,%f;4,%u;]", data.player_name, x, yextra + 1.3, bag_size) + fs"style_type[list;size=1;spacing=0.15]" +end + +local function get_container(fs, data, player, yoffset, ctn_len, award_list, awards_unlocked, award_list_nb, bag_size) + local nametag = player:get_nametag_attributes() + local name = true_str(nametag.text) and nametag.text or data.player_name + local esc_name = ESC(name) + + add_subtitle(fs, "player_name", 0, ctn_len, 22, true, esc_name) + + if damage_enabled then + local hp = data.hp or player:get_hp() or 20 + local half = ceil((hp / 2) % 1) + local hearts = (hp / 2) + half + local heart_size = 0.35 + local heart_x, heart_h = 0.65, yoffset + 0.75 + + for i = 1, 10 do + image(heart_x + ((i - 1) * (heart_size + 0.1)), heart_h, + heart_size, heart_size, PNG.heart .. "^[colorize:#232428") + end + + for i = 1, hearts do + image(heart_x + ((i - 1) * (heart_size + 0.1)), heart_h, + heart_size, heart_size, + (half == 1 and i == floor(hearts)) and PNG.heart_half or PNG.heart) + end + else + yoffset -= 0.5 + end + + fs("list[current_player;craft;%f,%f;3,3;]", 0, yoffset + 1.45) + image(3.47, yoffset + 2.69, 0.85, 0.85, PNG.arrow) + fs("list[current_player;craftpreview;%f,%f;1,1;]", 4.45, yoffset + 2.6) + fs("list[detached:i3_trash;main;%f,%f;1,1;]", 4.45, yoffset + 3.75) + image(4.45, yoffset + 3.75, 1, 1, PNG.trash) + + local yextra = damage_enabled and 5.5 or 5 + + for i, title in ipairs(i3.categories) do + local btn_name = fmt("btn_%s", title) + fs("style[btn_%s;fgimg=%s;fgimg_hovered=%s;content_offset=0]", title, + data.subcat == i and PNG[fmt("%s_hover", title)] or PNG[title], + PNG[fmt("%s_hover", title)]) + image_button(0.25 + ((i - 1) * 1.18), yextra - 0.2, 0.5, 0.5, "", btn_name, "") + fs("tooltip[%s;%s]", btn_name, title:gsub("^%l", upper)) + end + + box(0, yextra + 0.45, ctn_len, 0.045, "#bababa50") + image((data.subcat - 1) * 1.18, yextra + 0.45, 1, 0.045, PNG.highlight) + + local function not_installed(modname) + hypertext(0, yextra + 0.9, ctn_len, 0.6, "not_installed", + fmt("
not installed
", + colors.blue, modname)) + end + + if data.subcat == 1 then + get_bag_fs(fs, data, bag_size, yextra) + + elseif data.subcat == 2 then + if not i3.modules.armor then + return not_installed "3d_armor" + end + + local armor_def = armor.def[data.player_name] + local _, armor_inv = armor:get_valid_player(player, "3d_armor") + + fs("list[detached:%s_armor;armor;0,%f;5,1;]", esc_name, yextra + 0.7) + + for i = 1, 5 do + local stack = armor_inv:get_stack("armor", i) + + if stack:is_empty() then + local tips = {ES"Helmet", ES"Chest", ES"Leggings", ES"Boots", ES"Shield"} + local x = (i - 1) + ((i - 1) * 0.15) + local y = yextra + 0.7 + + image(x, y, 1, 1, fmt("i3_armor_%u.png", i)) + tooltip(x, y, 1, 1, tips[i]) + end + end + + local box_len, max_level, max_heal = 4, 85, 60 + local bar_lvl = (armor_def.level * box_len) / max_level + local bar_heal = (armor_def.heal * box_len) / max_heal + + fs"style_type[label;font_size=15]" + + box(0.8, yextra + 1.95, box_len, 0.4, "#101010") + fs"style_type[box;colors=#9dc34c80,#9dc34c,#9dc34c,#9dc34c80]" + box(0.8, yextra + 1.95, bar_lvl, 0.4, "") + label(1.1, yextra + 2.15, ES"Armor level") + + box(0.8, yextra + 2.55, box_len, 0.4, "#101010") + fs"style_type[box;colors=#4466aa80,#4466aa,#4466aa,#4466aa80]" + box(0.8, yextra + 2.55, bar_heal, 0.4, "") + label(1.1, yextra + 2.75, ES"Armor healing") + + fs"style_type[label;font_size=16]" + + elseif data.subcat == 3 then + if not i3.modules.skins then + return not_installed "skinsdb" + end + + local _skins = skins.get_skinlist_for_player(data.player_name) + local skin_name = skins.get_player_skin(player).name + local spp, add_y = 24, 0 + + if #_skins > spp then + local btn_y = yextra + 0.75 + add_y += 0.6 + + data.skin_pagemax = max(1, ceil(#_skins / spp)) + + image_button(1.5, btn_y, 0.35, 0.35, "", "prev_skin", "") + image_button(3.85, btn_y, 0.35, 0.35, "", "next_skin", "") + + fs"style[skin_page;font=bold;font_size=18]" + button(1.85, btn_y - 0.23, 2, 0.8, "skin_page", + fmt("%s / %u", clr(colors.yellow, data.skin_pagenum), data.skin_pagemax)) + end + + local first = (data.skin_pagenum - 1) * spp + local last = first + spp - 1 + + for i = first, last do + local skin = _skins[i + 1] + if not skin then break end + local btn_name = fmt("skin_btn_%u", i + 1) + + fs([[ style[%s;padding=10;fgimg=%s;bgimg=%s;bgimg_hovered=i3_btn9_hovered.png; + bgimg_pressed=i3_btn9_pressed.png;bgimg_middle=4,6;sound=] ]], + btn_name, skin:get_preview(), + skin.name == skin_name and "i3_btn9_hovered.png" or "i3_btn9.png") + + local X = (i % 3) * 1.93 + + local Y = ceil((i % spp - X) / 3 + 1) + Y += (Y * 2.45) + yextra - 2.75 + add_y + + image_button(X, Y, 1.86, 3.4, "", btn_name, "") + fs("tooltip[%s;%s]", btn_name, ESC(skin.name)) + end + + elseif data.subcat == 4 then + if not i3.modules.awards then + return not_installed "awards" + end + + yextra = yextra + 0.7 + get_award_list(data, fs, ctn_len, yextra, award_list, awards_unlocked, award_list_nb) + + elseif data.subcat == 5 then + get_waypoint_fs(fs, data, player, yextra, ctn_len) + end +end + +local function show_settings(fs, data) + if data.confirm_trash then + image(2.8, 10.65, 4.6, 0.7, PNG.bg_goto) + label(3.02, 11, "Confirm trash?") + image_button(5.07, 10.75, 1, 0.5, "", "confirm_trash_yes", "Yes") + image_button(6.17, 10.75, 1, 0.5, "", "confirm_trash_no", "No") + + elseif data.show_settings then + fs"container[-0.06,0]" + image(2.2, 9, 6.1, 2.35, PNG.bg_content) + + local show_home = data.show_setting == "home" + local show_style = data.show_setting == "style" + local show_sorting = data.show_setting == "sorting" + + fs"style[setting_home,setting_style,setting_sorting;font=bold;font_size=16;sound=i3_click]" + fs("style[setting_home:hovered;textcolor=%s]", show_home and colors.yellow or "#fff") + fs("style[setting_style:hovered;textcolor=%s]", show_style and colors.yellow or "#fff") + fs("style[setting_sorting:hovered;textcolor=%s]", show_sorting and colors.yellow or "#fff") + + fs("style[setting_home;bgimg=%s;bgimg_hovered=%s;bgimg_middle=9;padding=-9;textcolor=%s]", + show_home and PNG.pagenum_hover or "", PNG.pagenum_hover, + show_home and colors.yellow or "#ddd") + fs("style[setting_style;bgimg=%s;bgimg_hovered=%s;bgimg_middle=9;padding=-9;textcolor=%s]", + show_style and PNG.pagenum_hover or "", PNG.pagenum_hover, + show_style and colors.yellow or "#ddd") + fs("style[setting_sorting;bgimg=%s;bgimg_hovered=%s;bgimg_middle=9;padding=-9;textcolor=%s]", + show_sorting and PNG.pagenum_hover or "", PNG.pagenum_hover, + show_sorting and colors.yellow or "#ddd") + + local X = 2.5 + button(X, 9.1, 1.6, 0.55, "setting_home", "Home") + button(X + 1.7, 9.1, 1.6, 0.55, "setting_style", "Style") + button(X + 3.4, 9.1, 1.6, 0.55, "setting_sorting", "Sorting") + image_button(X + 5.12, 9.2, 0.25, 0.25, PNG.cancel_hover .. "^\\[brighten", "close_settings", "") + + if show_home then + local coords, c, str = {"X", "Y", "Z"}, 0, ES"No home set" + + if data.home then + str = data.home:gsub(",", " "):sub(2,-2):gsub("%.%d", ""):gsub( + "(%-?%d+)", function(a) + c++ + return fmt("%s: ", + coords[c], colors.blue, a) + end) + end + + hypertext(2.2, 9.9, 6, 0.6, "home_pos", fmt("
%s
", str)) + image_button(4.3, 10.4, 1.8, 0.7, "", "set_home", "Set home") + + elseif show_style then + checkbox(2.6, 9.95, "cb_hide_tabs", "Hide tabs", tostring(data.hide_tabs)) + checkbox(2.6, 10.4, "cb_legacy_inventory", "Legacy inventory", tostring(data.legacy_inventory)) + checkbox(2.6, 10.85, "cb_wielditem_hud", "HUD description", tostring(data.wielditem_hud)) + + if not recipe_filter_set() then + checkbox(5.3, 10.85, "cb_collapse", "Collapse list", tostring(data.collapse)) + end + + local sign = (data.font_size > 0 and "+") or (data.font_size > 0 and "-") or "" + label(5.3, 9.95, ES"Font size" .. fmt(": %s", sign .. data.font_size)) + + local range = 5 + fs("scrollbaroptions[min=-%u;max=%u;smallstep=1;largestep=1;thumbsize=2]", range, range) + fs("scrollbar[5.3,10.2;2.55,0.3;horizontal;sb_font_size;%d]", data.font_size) + + fs("tooltip[cb_hide_tabs;%s;#707070;#fff]", + ES"Enable this option to change the style of the right panel") + fs("tooltip[cb_legacy_inventory;%s;#707070;#fff]", + ES"Enable this option to set the classic inventory size in Minetest") + fs("tooltip[cb_wielditem_hud;%s;#707070;#fff]", + ES"Enable this option to show the wielded item description in your HUD") + fs("tooltip[cb_collapse;%s;#707070;#fff]", + ES"Enable this option to collapse the inventory list by grouping some items") + + elseif show_sorting then + checkbox(2.6, 9.95, "cb_inv_compress", "Compression", tostring(data.inv_compress)) + checkbox(2.6, 10.4, "cb_reverse_sorting", "Reverse mode", tostring(data.reverse_sorting)) + checkbox(2.6, 10.85, "cb_ignore_hotbar", "Ignore hotbar", tostring(data.ignore_hotbar)) + checkbox(5.3, 9.95, "cb_auto_sorting", "Automation", tostring(data.auto_sorting)) + + local methods = {} + + for _, v in ipairs(i3.sorting_methods) do + local name = toupper(v.name) + insert(methods, name) + end + + label(5.3, 10.4, ES"Sorting method:") + fs("dropdown[%f,%f;2.6,0.5;dd_sorting_method;%s;%u;true]", + 5.3, 10.6, concat(methods, ","), data.sort) + + local desc = i3.sorting_methods[data.sort].description + if desc then + tooltip(5.3, 10.6, 2.4, 0.5, ESC(desc)) + end + + fs("tooltip[cb_inv_compress;%s;#707070;#fff]", + ES"Enable this option to compress your inventory") + fs("tooltip[cb_reverse_sorting;%s;#707070;#fff]", + ES"Enable this option to sort your inventory in reverse order") + fs("tooltip[cb_ignore_hotbar;%s;#707070;#fff]", + ES"Enable this option to sort your inventory except the hotbar slots") + fs("tooltip[cb_auto_sorting;%s;#707070;#fff]", + ES"Enable this option to sort your inventory automatically") + end + + fs"container_end[]" + end +end + +local function get_inventory_fs(player, data, fs) + fs"listcolors[#bababa50;#bababa99]" + + get_inv_slots(data, fs) + + local props = player:get_properties() + local ctn_len = 5.7 + local ctn_hgt = data.legacy_inventory and 6.1 or 6.3 + local yoffset = 0 + + if props.mesh ~= "" then + local anim = player:get_local_animation() + local armor_skin = i3.modules.armor or i3.modules.skins + local t = {} + + for _, v in ipairs(props.textures) do + insert(t, (ESC(v):gsub(",", "!"))) + end + + local textures = concat(t, ","):gsub("!", ",") + + -- fs"style[player_model;bgcolor=black]" + model(0.2, 0.2, armor_skin and 4 or 3.4, ctn_hgt, + "player_model", props.mesh, textures, "0,-150", "false", "false", + fmt("%u,%u;30", anim.x, anim.y)) + else + local size = 2.5 + image(0.7, 0.2, size, size * props.visual_size.y, props.textures[1]) + end + + local awards_unlocked, award_list, award_list_nb = 0 + local max_val = damage_enabled and 12 or 7 + max_val += (data.legacy_inventory and 2 or 0) + local bag_size = get_group(ItemStack(data.bag):get_name(), "bag") + + if data.subcat == 1 and bag_size > 0 then + max_val += min(32, 6 + ((bag_size - 1) * 10)) + + elseif i3.modules.armor and data.subcat == 2 then + if data.scrbar_inv >= max_val then + data.scrbar_inv += 10 + end + + max_val += 10 + + elseif i3.modules.skins and data.subcat == 3 then + local spp = 24 + local _skins = skins.get_skinlist_for_player(data.player_name) + local nb = #_skins + local num = max(1, min(spp, nb - ((data.skin_pagenum - 1) * spp))) + + max_val += ceil(num / 3) * (nb > spp and 34 or 31) + + elseif i3.modules.awards and data.subcat == 4 then + award_list = awards.get_award_states(data.player_name) + award_list_nb = #award_list + + for i = 1, award_list_nb do + local award = award_list[i] + + if award.unlocked then + awards_unlocked++ + end + end + + max_val += (award_list_nb * 13) + + elseif data.subcat == 5 then + local wp = #data.waypoints + + if wp > 0 then + local mul = (wp > 8 and 7) or (wp > 4 and 6) or 5 + max_val += 11 + (wp * mul) + end + end + + fs([[ scrollbaroptions[arrows=hide;thumbsize=%d;max=%d] + scrollbar[%f,0.2;0.2,%f;vertical;scrbar_inv;%u] + scrollbaroptions[arrows=default;thumbsize=0;max=1000] ]], + (max_val * 4) / 12, max_val, 9.8, ctn_hgt, data.scrbar_inv) + + fs("scroll_container[3.9,0.2;%f,%f;scrbar_inv;vertical]", ctn_len, ctn_hgt) + get_container(fs, data, player, yoffset, ctn_len, award_list, awards_unlocked, award_list_nb, bag_size) + fs"scroll_container_end[]" + + local btn = { + {"trash", ES"Clear inventory"}, + {"sort", ES"Sort inventory"}, + {"settings", ES"Settings"}, + {"home", ES"Go home"}, + } + + for i, v in ipairs(btn) do + local btn_name, tooltip = unpack(v) + fs("style[%s;fgimg=%s;fgimg_hovered=%s;content_offset=0]", + btn_name, PNG[btn_name], PNG[fmt("%s_hover", btn_name)]) + image_button(i + 3.43 - (i * 0.4), 11.43, 0.35, 0.35, "", btn_name, "") + fs("tooltip[%s;%s]", btn_name, tooltip) + end + + show_settings(fs, data) +end + +local function get_tooltip(item, info, lang_code) + local tooltip + + if info.groups then + sort(info.groups) + tooltip = i3.group_names[concat(info.groups, ",")] + + if not tooltip then + local groupstr = {} + + for i = 1, #info.groups do + insert(groupstr, clr("#ff0", info.groups[i])) + end + + groupstr = concat(groupstr, ", ") + tooltip = S("Any item belonging to the groups: @1", groupstr) + end + else + tooltip = info.meta_desc or get_desc(item, lang_code) + end + + local function add(str) + return fmt("%s\n%s", tooltip, str) + end + + if info.cooktime then + tooltip = add(S("Cooking time: @1", clr("#ff0", info.cooktime))) + end + + if info.burntime then + tooltip = add(S("Burning time: @1", clr("#ff0", info.burntime))) + end + + if info.replace then + for i = 1, #info.replace.items do + local rpl = ItemStack(info.replace.items[i]):get_name() + local desc = clr("#ff0", get_desc(rpl, lang_code)) + + if info.replace.type == "cooking" then + tooltip = add(S("Replaced by @1 on smelting", desc)) + elseif info.replace.type == "fuel" then + tooltip = add(S("Replaced by @1 on burning", desc)) + else + tooltip = add(S("Replaced by @1 on crafting", desc)) + end + end + end + + if info.repair then + tooltip = add(S("Repairable by step of @1", clr("#ff0", i3.toolrepair .. "%"))) + end + + if info.rarity then + local chance = (1 / max(1, info.rarity)) * 100 + tooltip = add(S("@1 of chance to drop", clr("#ff0", chance .. "%"))) + end + + if info.tools then + local several = #info.tools > 1 + local names = several and "\n" or "" + + if several then + for i = 1, #info.tools do + names = fmt("%s\t\t- %s\n", names, clr("#ff0", get_desc(info.tools[i], lang_code))) + end + + tooltip = add(S("Only drop if using one of these tools: @1", sub(names, 1, -2))) + else + tooltip = add(S("Only drop if using this tool: @1", + clr("#ff0", get_desc(info.tools[1], lang_code)))) + end + end + + return fmt("tooltip[%s;%s]", item, ESC(tooltip)) +end + +local function get_true_count(data, count, is_recipe, is_usage) + local count_mul = 1 + + if is_recipe then + count_mul = data.scrbar_rcp + elseif is_usage then + count_mul = data.scrbar_usg + end + + if count_mul then + count *= count_mul + end + + return count +end + +local function get_output_fs(fs, data, rcp, is_recipe, is_usage, shapeless, right, btn_size, btn_size2) + local custom_recipe = i3.craft_types[rcp.type] + local cooking = rcp.type == "cooking" + local fuel = rcp.type == "fuel" + + if custom_recipe or shapeless or cooking then + local icon, tooltip = PNG.blank + + if custom_recipe and true_str(custom_recipe.icon) then + icon = custom_recipe.icon + elseif shapeless then + icon = PNG.shapeless + end + + local pos_x = right + btn_size + 0.42 + local pos_y = data.yoffset + 0.9 + + if cooking then + animated_image(pos_x, pos_y, 0.5, 0.5, PNG.furnace_anim, 8, 180) + else + image(pos_x, pos_y, 0.5, 0.5, icon) + end + + if custom_recipe and true_str(custom_recipe.description) then + tooltip = custom_recipe.description + elseif shapeless then + tooltip = S"Shapeless" + elseif cooking then + tooltip = S"Cooking" + end + + if tooltip then + tooltip(pos_x, pos_y, 0.5, 0.5, ESC(tooltip)) + end + end + + local BTN_SIZE = i3.settings.item_btn_size + local arrow_X = right + 0.2 + (btn_size2 or BTN_SIZE) + local X = arrow_X + 1.2 + local Y = data.yoffset + 1.4 + + image(arrow_X, Y + 0.06, 1, 1, PNG.arrow) + + if fuel then + animated_image(X + 0.05, Y, BTN_SIZE, BTN_SIZE, PNG.fire_anim, 8, 180) + return + end + + local item = ItemStack(rcp.output) + local meta = item:get_meta() + local name = item:get_name() + local count = item:get_count() + local _name = fmt("_%s", name) + + local size = BTN_SIZE * 1.2 + slot(X, Y - 0.11, size, size) + + count = get_true_count(data, count, is_recipe, is_usage) + item:set_count(count) + + local itemstr = ESC(item:to_string()) + item_image_button(X + 0.11, Y, BTN_SIZE, BTN_SIZE, itemstr, _name, "") + + local def = reg_items[name] + local unknown = not def or nil + local desc = def and def.description + local weird = name ~= "" and desc and weird_desc(desc) or nil + local burntime = i3.fuel_cache[name] and i3.fuel_cache[name].burntime + + local short_desc = meta:get_string"short_description" + local long_desc = meta:get_string"description" + local meta_desc = (short_desc ~= "" and short_desc) or (long_desc ~= "" and long_desc) + + local infos = { + unknown = unknown, + weird = weird, + burntime = burntime, + repair = repairable(name), + rarity = rcp.rarity, + tools = rcp.tools, + meta_desc = meta_desc, + } + + if next(infos) then + fs(get_tooltip(_name, infos, data.lang_code)) + end +end + +local function get_grid_fs(fs, data, rcp, is_recipe, is_usage) + local width = rcp.width or 1 + local right = 0 + local btn_size, _btn_size = i3.settings.item_btn_size + local cooktime, shapeless + + if rcp.type == "cooking" then + cooktime, width = width, 1 + elseif width == 0 and not rcp.custom then + shapeless = true + local n = #rcp.items + width = (n < 5 and n > 1) and 2 or min(3, max(1, n)) + end + + local rows = ceil(maxn(rcp.items) / width) + local large_recipe = width > 3 or rows > 3 + + if large_recipe then + fs"style_type[item_image_button;border=true]" + end + + for i = 1, width * rows do + local item = rcp.items[i] or "" + item = ItemStack(item) + local meta = item:get_meta() + local name = item:get_name() + local count = item:get_count() + local X, Y + + if large_recipe then + local a, b = 3, 3 + local add_x, add_y = 0, 0 + + if width < 3 then + a, b = width * 2, 1 + add_x = 2 + elseif rows < 3 then + a, b = 1, rows * 2 + add_y = 1.4 + end + + btn_size = (a / width) * (b / rows) + 0.3 + _btn_size = btn_size + + local xi = (i - 1) % width + local yi = floor((i - 1) / width) + + X = btn_size * xi + data.inv_width + 0.3 + (xi * 0.05) + add_x + Y = btn_size * yi + data.yoffset + 0.2 + (yi * 0.05) + add_y + else + X = ceil((i - 1) % width - width) + X += (X * 0.2) + data.inv_width + 3.9 + + Y = ceil(i / width) - min(2, rows) + Y += (Y * 0.15) + data.yoffset + 1.4 + end + + if X > right then + right = X + end + + local groups + local group_cache = i3.groups[name:sub(7)] + + if is_group(name) then + groups = group_cache and group_cache.groups or extract_groups(name) + name = group_cache and (group_cache.stereotype or group_cache.items[1]) or + groups_to_items(groups)[1] or "" + end + + local label = groups and "\nG" or "" + local replace + + for j = 1, #(rcp.replacements or {}) do + local replacement = rcp.replacements[j] + if replacement[1] == name then + replace = replace or {type = rcp.type, items = {}} + + local added + + for _, v in ipairs(replace.items) do + if replacement[2] == v then + added = true + break + end + end + + if not added then + label = fmt("%s%s\nR", label ~= "" and "\n" or "", label) + insert(replace.items, replacement[2]) + end + end + end + + if not large_recipe then + slot(X, Y, btn_size, btn_size) + end + + local btn_name = groups and fmt("group!%s!%s", groups[1], name) or name + count = get_true_count(data, count, is_recipe, is_usage) + + if group_cache and group_cache.sprite and not large_recipe then + local sprite = ESC(group_cache.sprite) + local size = btn_size - 0.02 + + item_image_button(X, Y, btn_size, btn_size, "", btn_name, "") + animated_image(X + 0.01, Y + 0.025, size, size, sprite, group_cache.count, 1500) + label(X + 0.45, Y + 0.18, label) + + if count > 1 then + label(X + 0.8, Y + 0.9, count) + end + else + item:set_name(name) + item:set_count(count) + local itemstr = ESC(item:to_string()) + item_image_button(X, Y, btn_size, btn_size, itemstr, btn_name, label) + end + + local def = reg_items[name] + local unknown = not def or nil + unknown = not groups and unknown or nil + local desc = def and def.description + local weird = name ~= "" and desc and weird_desc(desc) or nil + local burntime = i3.fuel_cache[name] and i3.fuel_cache[name].burntime + + local short_desc = meta:get_string"short_description" + local long_desc = meta:get_string"description" + local meta_desc = (short_desc ~= "" and short_desc) or (long_desc ~= "" and long_desc) or nil + + local infos = { + unknown = unknown, + weird = weird, + groups = groups, + burntime = burntime, + cooktime = cooktime, + replace = replace, + meta_desc = meta_desc, + } + + if next(infos) then + fs(get_tooltip(btn_name, infos, data.lang_code)) + end + end + + if large_recipe then + fs"style_type[item_image_button;border=false]" + end + + get_output_fs(fs, data, rcp, is_recipe, is_usage, shapeless, right, btn_size, _btn_size) +end + +local function get_rcp_lbl(fs, data, panel, rn, is_recipe, is_usage) + local rcp = is_recipe and panel.rcp[data.rnum] or panel.rcp[data.unum] + + if rcp.custom then + local craft_type = i3.craft_types[rcp.type] + if craft_type then + local desc = craft_type.description + hypertext(data.inv_width + 4.8, data.yoffset + 0.12, 3, 1, "custom_rcp", + fmt("%s\n%s", + ES"Custom recipe", ESC(desc))) + end + end + + local lbl = ES("Usage @1 of @2", data.unum, rn) + + if is_recipe then + lbl = ES("Recipe @1 of @2", data.rnum, rn) + end + + local one = rn == 1 + local y = data.yoffset + 3.3 + + hypertext(data.inv_width + (one and 4.7 or 3.95), y, 3, 0.6, "rcp_num", + fmt("%s", lbl)) + + if not one then + local btn_suffix = is_recipe and "recipe" or "usage" + local prev_name = fmt("prev_%s", btn_suffix) + local next_name = fmt("next_%s", btn_suffix) + local size = 0.3 + + image_button(data.inv_width + 7.05, y, size, size, "", prev_name, "") + image_button(data.inv_width + 7.5, y, size, size, "", next_name, "") + end + + get_grid_fs(fs, data, rcp, is_recipe, is_usage) +end + +local function get_model_fs(fs, data, def, model_alias) + if model_alias then + if model_alias.drawtype == "entity" then + def = reg_entities[model_alias.name] + local init_props = def.initial_properties + def.textures = init_props and init_props.textures or def.textures + def.mesh = init_props and init_props.mesh or def.mesh + else + def = reg_items[model_alias.name] + end + end + + local tiles = def.tiles or def.textures or {} + local t = {} + + for _, v in ipairs(tiles) do + local _name + + if v.color then + if is_num(v.color) then + local hex = fmt("%02x", v.color) + + while #hex < 8 do + hex = "0" .. hex + end + + _name = fmt("%s^[multiply:%s", v.name, fmt("#%s%s", sub(hex, 3), sub(hex, 1, 2))) + else + _name = fmt("%s^[multiply:%s", v.name, v.color) + end + elseif v.animation then + _name = fmt("%s^[verticalframe:%u:0", v.name, v.animation.frames_h or v.animation.aspect_h) + end + + insert(t, _name or v.name or v) + end + + while #t < 6 do + insert(t, t[#t]) + end + + model(data.inv_width + 6.6, data.yoffset + 0.05, 1.3, 1.3, "preview", + def.mesh, concat(t, ","), "0,0", "true", "true", + model_alias and model_alias.frames or "") +end + +local function get_header(fs, data) + local fav = is_fav(data) + local nfavs = #data.favs + local max_favs = i3.settings.max_favs + local star_x, star_y, size = data.inv_width + 0.3, data.yoffset + 0.2, 0.4 + + if nfavs < max_favs or (nfavs == max_favs and fav) then + local fav_marked = fmt("i3_fav%s.png", fav and "_off" or "") + fs("style[fav;fgimg=%s;fgimg_hovered=%s;fgimg_pressed=%s]", + fmt("i3_fav%s.png", fav and "" or "_off"), fav_marked, fav_marked) + image_button(star_x, star_y, size, size, "", "fav", "") + fs("tooltip[fav;%s]", fav and ES"Unmark this item" or ES"Mark this item") + else + fs("style[nofav;fgimg=%s;fgimg_hovered=%s;fgimg_pressed=%s]", + "i3_fav_off.png", PNG.cancel, PNG.cancel) + image_button(star_x, star_y, size, size, "", "nofav", "") + fs("tooltip[nofav;%s]", ES"Cannot mark this item. Bookmark limit reached.") + end + + image_button(star_x + 0.05, star_y + 0.6, size, size, "", "exit", "") + fs("tooltip[exit;%s]", ES"Back to item list") + + local desc_lim, name_lim = 34, 35 + local desc = get_desc(data.query_item, data.lang_code) + desc = ESC(desc) + local tech_name = data.query_item + local X = data.inv_width + 0.95 + local Y1 = data.yoffset + 0.47 + local Y2 = Y1 + 0.5 + + local _desc = snip(desc, desc_lim, data.font_size) + if _desc then + tooltip(X, Y1 - 0.1, 5.7, 0.24, desc) + end + + local _tech_name = snip(tech_name, name_lim, data.font_size) + if _tech_name then + tooltip(X, Y2 - 0.1, 5.7, 0.24, tech_name) + end + + fs"style_type[label;font=bold;font_size=20]" + label(X, Y1, _desc or desc) + fs"style_type[label;font=mono;font_size=16]" + label(X, Y2, clr(colors.blue, _tech_name or tech_name)) + fs"style_type[label;font=normal;font_size=16]" + + local def = reg_items[data.query_item] + local model_alias = model_aliases[data.query_item] + + if def.drawtype == "mesh" or model_alias then + get_model_fs(fs, data, def, model_alias) + else + item_image(data.inv_width + 6.8, data.yoffset + 0.17, 1.1, 1.1, data.query_item) + end +end + +local function get_crafting_fs(fs, data, is_recipe, is_usage, max_stacks_rcp, max_stacks_usg) + local name = is_recipe and "rcp" or "usg" + local show_crafting = (is_recipe and data.crafting_rcp) or (is_usage and data.crafting_usg) + + fs("style[crafting_%s;fgimg=%s;fgimg_hovered=%s;content_offset=0]", + name, fmt("%s", show_crafting and PNG.crafting_hover or PNG.crafting), PNG.crafting_hover) + image_button(data.inv_width + 7.35, data.yoffset + 0.2, 0.45, 0.45, "", fmt("crafting_%s", name), "") + fs("tooltip[crafting_%s;%s]", name, ES"Quick crafting") + + if not show_crafting then return end + + local craft_max = is_recipe and max_stacks_rcp or max_stacks_usg + local stack_fs = (is_recipe and data.scrbar_rcp) or (is_usage and data.scrbar_usg) or 1 + + if stack_fs > craft_max then + stack_fs = craft_max + + if is_recipe then + data.scrbar_rcp = craft_max + elseif is_usage then + data.scrbar_usg = craft_max + end + end + + local x = data.inv_width + 6.8 + + fs"style_type[image,button,image_button;noclip=true]" + image(x, data.yoffset + 0.8, 3, 2, PNG.bg_content) + fs"style[quick_crafting;font_size=16;textcolor=#ddd]" + button(x, data.yoffset + 0.85, 3.05, 0.55, "quick_crafting", ES"Quick Crafting") + + fs("style[scrbar_%s;noclip=true]", name) + fs("scrollbaroptions[min=1;max=%u;smallstep=1]", craft_max) + + scrollbar(x + 0.2, data.yoffset + 1.45, 2.5, 0.35, "horizontal", fmt("scrbar_%s", name), stack_fs) + button(x + 0.2, data.yoffset + 1.85, 2.5, 0.7, fmt("craft_%s", name), ES("Craft (×@1)", stack_fs)) + + fs"style_type[label;font_size=16;textcolor=#fff]" +end + +local function get_rcp_extra(fs, data, player, panel, is_recipe, is_usage) + fs"container[0,0.075]" + local rn = panel.rcp and #panel.rcp + + if rn then + local rcp_ok = is_recipe and panel.rcp[data.rnum].type == "normal" + local usg_ok = is_usage and panel.rcp[data.unum].type == "normal" + local max_stacks_rcp, max_stacks_usg = 0, 0 + local inv = player:get_inventory() + + if rcp_ok then + max_stacks_rcp = get_stack_max(inv, data, is_recipe, panel.rcp[data.rnum]) + end + + if usg_ok then + max_stacks_usg = get_stack_max(inv, data, is_recipe, panel.rcp[data.unum]) + end + + if is_recipe and max_stacks_rcp == 0 then + data.crafting_rcp = nil + data.scrbar_rcp = 1 + elseif is_usage and max_stacks_usg == 0 then + data.crafting_usg = nil + data.scrbar_usg = 1 + end + + if max_stacks_rcp > 0 or max_stacks_usg > 0 then + get_crafting_fs(fs, data, is_recipe, is_usage, max_stacks_rcp, max_stacks_usg) + end + + get_rcp_lbl(fs, data, panel, rn, is_recipe, is_usage) + else + local lbl = is_recipe and ES"No recipes" or ES"No usages" + button(data.inv_width + 0.1, data.yoffset + (panel.height / 2) - 0.5, 7.8, 1, "no_rcp", lbl) + end + + fs"container_end[]" +end + +local function hide_items(player, data) + if compression_active(data) then + local new = {} + + for i = 1, #data.items do + local item = data.items[i] + if not i3.compressed[item] then + insert(new, item) + end + end + + data.items = new + end + + if not core.is_creative_enabled(data.player_name) and not recipe_filter_set() then + local new = {} + + for i = 1, #data.items do + local item = data.items[i] + local recipes, usages = get_recipes(player, item) + + if recipes or usages then + insert(new, item) + end + end + + data.items = new + end +end + +local function get_header_items_fs(fs, data) + local X = data.inv_width + fs"set_focus[filter;true]" + + if data.hide_tabs then + fs("style[enable_search;bgimg=%s;bgimg_hovered=%s;bgimg_pressed=%s]", + data.enable_search and PNG.search_hover or PNG.search, PNG.search_hover, PNG.search_hover) + image_button(X + 0.3, 0.2, 0.5, 0.5, "", "enable_search", "") + fs("tooltip[enable_search;%s]", ES"Search") + + if data.enable_search then + image(X + 0.4, 0.75, 3.4, 0.8, PNG.bg_goto) + + fs"style[filter;font_size=16]" + fs("field[%f,%f;3,0.45;filter;;%s]", X + 0.6, 0.95, data.filter) + fs"field_close_on_enter[filter;false]" + end + + box(X + 1, 0.2, 0.01, 0.5, "#bababa50") + local cat = {{"all", "all items"}, {"node", "nodes only"}, {"item", "items only"}} + + for i in ipairs(cat) do + local name, desc = unpack(cat[i]) + local active = PNG[name .. "_hover"] + + fs("style[itab_%u;bgimg=%s;bgimg_hovered=%s;bgimg_pressed=%s;sound=i3_tab]", + i, data.itab == i and active or PNG[name], active, active) + image_button(X + 1.25 + ((i - 1) * 0.7), 0.2, 0.5, 0.5, "", fmt("itab_%s", i), "") + fs("tooltip[itab_%u;Show %s]", i, desc) + end + else + fs("style[search;bgimg=%s]", PNG.search_hover) + image_button(X + 0.35, 0.32, 0.35, 0.35, "", "search", "") + fs("tooltip[search;%s]", ES"Search") + + if data.enable_search then + fs"style[filter;font_size=18]" + fs("field[%f,0.2;3.35,0.6;filter;;%s]", X + 0.85, ESC(data.filter)) + fs"field_close_on_enter[filter;false]" + + if not true_str(data.filter) then + image(X + 0.85, 0.75, 4, 0.01, PNG.search_outline_trim .. "^[opacity:100") + end + else + fs"style_type[label;font=italic;font_size=18]" + label(X + 0.9, 0.49, clr("#aaa", ES"Search...")) + button(X + 0.8, 0.12, 4, 0.8, "enable_search", "") + fs"style_type[label;font=normal;font_size=16]" + end + + if true_str(data.filter) then + image_button(X + 4.3, 0.4, 0.2, 0.2, "", "cancel", "") + fs("tooltip[cancel;%s]", ES"Clear") + box(X + 0.85, 0.75, 3.74, 0.01, "#f9826c") + end + end + + image_button(X + 5.27, 0.3, 0.35, 0.35, "", "prev_page", "") + image_button(X + 7.45, 0.3, 0.35, 0.35, "", "next_page", "") + + fs("style[pagenum;bgimg=%s;bgimg_hovered=%s;bgimg_middle=9;padding=-9;sound=i3_click]", + data.goto_page and PNG.pagenum_hover or "", PNG.pagenum_hover) + + button(X + 5.8, 0.14, 1.48, 0.7, "pagenum", + fmt("%s / %u", clr(colors.yellow, data.pagenum), data.pagemax)) + + if data.goto_page then + image(X + 4.8, 0.85, 2.9, 0.8, PNG.bg_goto) + fs"style_type[label;font_size=16;textcolor=#ddd]" + label(X + 5, 1.25, ES"Go to page" .. ":") + box(X + 6.5, 1, 1, 0.45, "#bababa10") + + fs("style[goto_page;font=mono,bold;font_size=16;textcolor=%s]", colors.yellow) + fs("field[%f,%f;1,0.45;goto_page;;%s]", X + 6.55, 1.05, data.pagenum) + fs"field_close_on_enter[goto_page;false]" + + fs"style_type[label;font_size=16;textcolor=#fff]" + end +end + +local function get_minitabs(fs, data, full_height) + local _tabs = {"All", "Nodes", "Items"} + local tab_len, tab_hgh = 1.8, 0.5 + + for i, title in ipairs(_tabs) do + local selected = i == data.itab + local hover_texture = selected and PNG.tab_small_hover or PNG.tab_small + + fs([[ style_type[image_button;bgimg=%s;bgimg_hovered=%s; + bgimg_middle=14,0,-14,-14;padding=-14,0,14,14] ]], hover_texture, PNG.tab_small_hover) + + fs([[ style_type[image_button;noclip=true;font=bold;font_size=16; + textcolor=%s;content_offset=0;sound=i3_tab] ]], selected and "#fff" or "#bbb") + fs"style_type[image_button:hovered;textcolor=#fff]" + image_button((data.inv_width - 0.65) + (i * (tab_len + 0.1)), + full_height, tab_len, tab_hgh, "", fmt("itab_%u", i), title) + end +end + +local function get_items_fs(fs, data, player, full_height) + hide_items(player, data) + bg9(data.inv_width + 0.1, 0, 7.9, full_height, PNG.bg_full) + + local items = data.alt_items or data.items or {} + local rows, lines = 8, 12 + local ipp = rows * lines + local size = 0.85 + + data.pagemax = max(1, ceil(#items / ipp)) + + if data.pagenum > data.pagemax then + data.pagenum = data.pagemax + end + + if #items == 0 then + local lbl = ES"No item to show" + local icon, width, offset = PNG.no_result, 4, 2 + + if recipe_filter_set() and #i3.init_items > 0 and data.filter == "" then + lbl = ES"Collect items to reveal more recipes" -- Progressive mode, etc. + icon, width, offset = PNG.find_more, 2.5, 2.75 + end + + image(data.inv_width + offset, 3.5, width, width, icon) + button(data.inv_width + 0.1, 7, 8, 1, "no_item", lbl) + else + local first_item = (data.pagenum - 1) * ipp + + for i = first_item, first_item + ipp - 1 do + local item = items[i + 1] + if not item then break end + + local _compressed = item:sub(1, 1) == "_" + local name = _compressed and item:sub(2) or item + + local X = i % rows + X -= (X * 0.045) + data.inv_width + 0.28 + + local Y = round((i % ipp - X) / rows + 1, 0) + Y -= (Y * 0.085) + 0.92 + + local item_btn = fmt("item_image_button", X, Y, size, size, name, item, "") + + if recipe_filter_set() then + if data.items_progress[item] then + insert(fs, item_btn) + else + local col = "^\\[colorize:#232428^\\[opacity:245" + local img = reg_items[item].inventory_image .. col + local nodedef = reg_nodes[item] + + if nodedef and not true_str(nodedef.inventory_image) then + img = PNG.cube .. col + end + + insert(fs, fmt("image", X, Y, size, size, img)) + end + else + insert(fs, item_btn) + end + + if compressible(item, data) then + local expand = data.expand == name + + fs("tooltip[%s;%s]", item, expand and ES"Click to hide" or ES"Click to expand") + fs"style_type[label;font=bold;font_size=20]" + label(X + 0.65, Y + 0.7, expand and "-" or "+") + fs"style_type[label;font=normal;font_size=16]" + end + end + end + + get_header_items_fs(fs, data) +end + +local function get_favs(fs, data) + local btn_size = i3.settings.item_btn_size + label(data.inv_width + 0.4, data.yoffset + 0.4, ES"Bookmarks") + + for i, item in ipairs(data.favs) do + local name = fmt("_%s", item) + local X = data.inv_width - 0.7 + (i * 1.2) + local Y = data.yoffset + 0.8 + + if data.query_item == item then + slot(X, Y, btn_size, btn_size) + end + + item_image_button(X, Y, btn_size, btn_size, item, name, "") + end +end + +local function get_panels(fs, data, player) + local title = {name = "title", height = 1.4, func = get_header} + local favs = {name = "favs", height = 2.23, func = get_favs} + local recipes = {name = "recipes", rcp = data.recipes, height = 4.045, func = get_rcp_extra} + local usages = {name = "usages", rcp = data.usages, height = 4.045, func = get_rcp_extra} + local panels = {title, recipes, usages, favs} + data.yoffset = 0 + + for i, panel in ipairs(panels) do + if i > 1 then + data.yoffset += panels[i - 1].height + 0.1 + end + + bg9(data.inv_width + 0.1, data.yoffset, 7.9, panel.height, PNG.bg_full) + + local is_recipe, is_usage = panel.name == "recipes", panel.name == "usages" + panel.func(fs, data, player, panel, is_recipe, is_usage) + end +end + +local function get_tabs_fs(fs, player, data, full_height) + local tab_len, tab_hgh, c, over = 3, 0.5, 0 + local _tabs = copy(i3.tabs) + + for i, def in ipairs(i3.tabs) do + if def.access and not def.access(player, data) then + remove(_tabs, i) + end + end + + local shift = min(3, #_tabs) + + for i, def in ipairs(_tabs) do + if not over and c > 2 then + over = true + c = 0 + end + + local btm = i <= 3 + + if not btm then + shift = #_tabs - 3 + end + + local selected = i == data.tab + local bgimg = selected and (btm and PNG.tab_hover or PNG.tab_hover_top) or + (btm and PNG.tab or PNG.tab_top) + local bgimg_hover = btm and PNG.tab_hover or PNG.tab_hover_top + + local middle = btm and "16,0,-16,-16" or "16,16,-16,-16" + local padding = btm and "-16,0,16,16" or "-16,-16,16,16" + + fs([[ style_type[image_button;bgimg=%s;bgimg_hovered=%s;bgimg_middle=%s;padding=%s] ]], + bgimg, bgimg_hover, middle, padding) + + fs("style_type[image_button;noclip=true;font_size=16;textcolor=%s;content_offset=0;sound=i3_tab]", + selected and "#fff" or "#ddd") + + local X = (data.inv_width / 2) + (c * (tab_len + 0.1)) - ((tab_len + 0.05) * (shift / 2)) + local Y = btm and full_height or -tab_hgh + + fs"style_type[image_button:hovered;textcolor=#fff]" + image_button(X, Y, tab_len, tab_hgh, "", fmt("tab_%s", def.name), ESC(def.description)) + + if true_str(def.image) then + local desc = translate(data.lang_code, def.description) + local desc_len = utf8_len(desc) + data.font_size + + fs"style_type[image;noclip=true]" + image(X + (tab_len / 2) - ((desc_len * 0.1) / 2) - 0.55, Y + 0.05, 0.35, 0.35, def.image) + end + + c++ + end +end + +local function get_debug_grid(data, fs, full_height) + fs"style[hide_debug_grid;noclip=true]" + button(-2, full_height - 1, 2, 1, "hide_debug_grid", "Toggle grid") + if data.hide_debug_grid then return end + + fs"style_type[label;font_size=8;noclip=true]" + local spacing, i = 0.2, 1 + + for x = 0, data.inv_width + 8, spacing do + box(x, 0, 0.01, full_height, "#ff0") + label(x, full_height + 0.1, tostring(i)) + i++ + end + + i = 61 + + for y = 0, full_height, spacing do + box(0, y, data.inv_width + 8, 0.01, "#ff0") + label(-0.15, y, tostring(i)) + i -= 1 + end + + box(data.inv_width / 2, 0, 0.01, full_height, "#f00") + box(0, full_height / 2, data.inv_width, 0.01, "#f00") + fs"style_type[label;font_size=16]" +end + +local function make_fs(player, data) + local start = debug_mode and core.get_us_time() or nil + + local fs = setmetatable({}, { + __call = function(t, ...) + local args = {...} + + if #args > 1 then + local arg1 = args[1] + local elem = fs_elements[arg1] + insert(t, fmt(elem or arg1, select(2, ...))) + else + insert(t, concat(args)) + end + end + }) + + data.inv_width = 10.23 + local full_height = 12 + + fs("formspec_version[%u]size[%f,%f]no_prepend[]bgcolor[#0000]", + i3.settings.min_fs_version, data.inv_width + 8, full_height) + + fs(styles) + + bg9(0, 0, data.inv_width, full_height, PNG.bg_full) + + local tab = i3.tabs[data.tab] + + if tab and tab.formspec then + tab.formspec(player, data, fs) + end + + if data.query_item then + get_panels(fs, data, player) + else + get_items_fs(fs, data, player, full_height) + + if not data.hide_tabs then + get_minitabs(fs, data, full_height) + end + end + + local visible_tabs = #i3.tabs + + for _, def in ipairs(i3.tabs) do + if def.access and not def.access(player, data) then + visible_tabs -= 1 + end + end + + if visible_tabs > 1 then + get_tabs_fs(fs, player, data, full_height) + end + + if debug_mode then + get_debug_grid(data, fs, full_height) + msg(data.player_name, fmt("make_fs(): %.2f ms", (core.get_us_time() - start) / 1000)) + msg(data.player_name, fmt("#fs elements: %u", #fs)) + end + + fs = concat(fs) + + if data.font_size ~= 0 then + fs = fs:gsub("([font][global]*)([%s_])size=(%d+)", function(a, b, c) + return fmt("%s%ssize=%s", a, b, tostring(tonumber(c) + data.font_size)) + end) + end + + return fs +end + +return make_fs, get_inventory_fs diff --git a/mods/i3/src/hud.lua b/mods/i3/src/hud.lua new file mode 100644 index 0000000..e790851 --- /dev/null +++ b/mods/i3/src/hud.lua @@ -0,0 +1,177 @@ +IMPORT("ceil", "get_connected_players", "str_to_pos", "add_hud_waypoint") + +local function init_hud(player) + local name = player:get_player_name() + local data = i3.data[name] + + local wdesc_y = -90 + + if core.global_exists"hb" then + wdesc_y -= ceil(hb.hudbars_count / 2) * 5 + elseif not i3.settings.damage_enabled then + wdesc_y += 15 + end + + data.hud = { + bg = player:hud_add { + hud_elem_type = "image", + position = {x = 0.78, y = 1}, + alignment = {x = 1, y = 1}, + scale = {x = 370, y = 112}, + text = "i3_bg.png", + z_index = 0xDEAD, + }, + + img = player:hud_add { + hud_elem_type = "image", + position = {x = 0.79, y = 1.02}, + alignment = {x = 1, y = 1}, + scale = {x = 1, y = 1}, + text = "", + z_index = 0xDEAD, + }, + + text = player:hud_add { + hud_elem_type = "text", + position = {x = 0.84, y = 1.04}, + alignment = {x = 1, y = 1}, + number = 0xffffff, + text = "", + z_index = 0xDEAD, + style = 1, + }, + + wielditem = player:hud_add { + hud_elem_type = "text", + position = {x = 0.5, y = 1}, + offset = {x = 0, y = wdesc_y}, + alignment = {x = 0, y = -1}, + number = 0xffffff, + text = "", + z_index = 0xDEAD, + style = 1, + }, + } +end + +local function show_hud(player, data) + -- It would better to have an engine function `hud_move` to only need + -- 2 calls for the notification's back and forth. + + local hud_info_bg = player:hud_get(data.hud.bg) + local dt = 0.025 + + if hud_info_bg.position.y <= 0.9 then + data.show_hud = false + data.hud_timer = (data.hud_timer or 0) + dt + end + + player:hud_change(data.hud.text, "text", data.hud_msg) + + if data.hud_img then + player:hud_change(data.hud.img, "text", data.hud_img) + end + + if data.show_hud then + for _, def in pairs(data.hud) do + local hud_info = player:hud_get(def) + + player:hud_change(def, "position", { + x = hud_info.position.x, + y = hud_info.position.y - ((dt / 5) * i3.settings.hud_speed) + }) + end + + elseif data.show_hud == false then + if data.hud_timer >= i3.settings.hud_timer_max then + for _, def in pairs(data.hud) do + local hud_info = player:hud_get(def) + + player:hud_change(def, "position", { + x = hud_info.position.x, + y = hud_info.position.y + ((dt / 5) * i3.settings.hud_speed) + }) + end + + if hud_info_bg.position.y >= 1 then + data.show_hud = nil + data.hud_timer = nil + data.hud_msg = nil + data.hud_img = nil + end + end + end +end + +core.register_globalstep(function(dt) + local players = get_connected_players() + players[0] = #players + + for i = 1, players[0] do + local player = players[i] + local name = player:get_player_name() + local data = i3.data[name] + if not data then return end + + if not data.wielditem_hud then + player:hud_change(data.hud.wielditem, "text", "") + return + end + + data.timer = (data.timer or 0) + dt + local wieldidx = player:get_wield_index() + + if wieldidx == data.old_wieldidx then + if data.timer >= i3.settings.wielditem_fade_after then + player:hud_change(data.hud.wielditem, "text", "") + end + return + end + + data.timer = 0 + data.old_wieldidx = wieldidx + + local wielditem = player:get_wielded_item() + local meta = wielditem:get_meta() + + local meta_desc = meta:get_string"short_description" + meta_desc = meta_desc:gsub("\27", "") + meta_desc = core.strip_colors(meta_desc) + + local desc = meta_desc ~= "" and meta_desc or wielditem:get_short_description() + player:hud_change(data.hud.wielditem, "text", desc:trim()) + end +end) + +core.register_globalstep(function() + local players = get_connected_players() + players[0] = #players + + for i = 1, players[0] do + local player = players[i] + local name = player:get_player_name() + local data = i3.data[name] + + if data and data.show_hud ~= nil then + show_hud(player, data) + end + end +end) + +local function init_waypoints(player) + local name = player:get_player_name() + local data = i3.data[name] + data.waypoints = data.waypoints or {} + + for _, v in ipairs(data.waypoints) do + if not v.hide then + local id = add_hud_waypoint(player, v.name, str_to_pos(v.pos), v.color) + v.id = id + end + end +end + +return function(player) + init_hud(player) + init_waypoints(player) +end diff --git a/mods/i3/src/model_aliases.lua b/mods/i3/src/model_aliases.lua new file mode 100644 index 0000000..f1d6699 --- /dev/null +++ b/mods/i3/src/model_aliases.lua @@ -0,0 +1,11 @@ +return { + ["boats:boat"] = {name = "boats:boat", drawtype = "entity"}, + ["carts:cart"] = {name = "carts:cart", drawtype = "entity", frames = "0,0"}, + ["default:chest"] = {name = "default:chest_open"}, + ["default:chest_locked"] = {name = "default:chest_locked_open"}, + ["doors:door_wood"] = {name = "doors:door_wood_a"}, + ["doors:door_glass"] = {name = "doors:door_glass_a"}, + ["doors:door_obsidian_glass"] = {name = "doors:door_obsidian_glass_a"}, + ["doors:door_steel"] = {name = "doors:door_steel_a"}, + ["xpanes:door_steel_bar"] = {name = "xpanes:door_steel_bar_a"}, +} diff --git a/mods/i3/src/preprocessor.lua b/mods/i3/src/preprocessor.lua new file mode 100644 index 0000000..138842e --- /dev/null +++ b/mods/i3/src/preprocessor.lua @@ -0,0 +1,98 @@ +--[[ All source files have to be preprocessed before loading. + This allows implementing custom operators like bitwise ones. ]] + +local fmt, split = string.format, string.split +local var = "[%w%.%[%]\"\'_]" +local modpath = core.get_modpath"i3" +local _,_, fs_elements = dofile(modpath .. "/src/styles.lua") + +local operators = { + ["([%+%-%*%^/&|])="] = function(a, b, c) + return fmt("%s = %s %s %s", a, a, b, c) + end, + + ["+%+"] = function(a, b) + return fmt("%s = %s + 1\n%s", a, a, b) + end, + + ["&"] = function(a, b) + return fmt("bit.band(%s, %s)", a, b) + end, + + ["|"] = function(a, b) + return fmt("bit.bor(%s, %s)", a, b) + end, + + ["<<"] = function(a, b) + return fmt("bit.lshift(%s, %s)", a, b) + end, + + [">>"] = function(a, b) + return fmt("bit.rshift(%s, %s)", a, b) + end, + + ["<<="] = function(a, b) + return fmt("%s = bit.lshift(%s, %s)", a, a, b) + end, + + [">>="] = function(a, b) + return fmt("%s = bit.rshift(%s, %s)", a, a, b) + end, +} + +local function compile(data) + data = data:gsub("IMPORT%((.-)%)", function(a) + return "local " .. a:gsub("\"", "") .. " = i3.get(" .. a .. ")" + end) + + data = data:gsub("([%w_]+)%(", function(a) + if fs_elements[a] then + return fmt("fs('%s',", a) + end + end) + + data = data:gsub("([%w_]+)-%-\n", function(a) + return fmt("%s = %s - 1", a, a) + end) + + for op, func in pairs(operators) do + data = data:gsub("(" .. var .. "+)%s?" .. op .. "%s?(" .. var .. "*)", func) + end + + return data +end + +local function _load(path, line, data, t) + if line then + if not t then + t = split(data, "\n") + end + t[line] = t[line]:gsub("(" .. var .. "+)%s?=%s?(" .. var .. "*)", "%2") + data = table.concat(t, "\n") + else + local file = assert(io.open(path, "r")) + data = file:read"*a" + file:close() + data = compile(data) + end + + local l, err = loadstring(data) + + if not l then + local err_line = tonumber(err:match(":(%d+):")) + + if t then + print("err_line", err_line, t[err_line]) + end + + if line ~= err_line then + return _load(path, err_line, data, t) + end + end + + return l, err +end + +return function(path) + return _load(path) or loadfile(path) +end diff --git a/mods/i3/src/progressive.lua b/mods/i3/src/progressive.lua new file mode 100644 index 0000000..1797aa1 --- /dev/null +++ b/mods/i3/src/progressive.lua @@ -0,0 +1,169 @@ +local set_fs = i3.set_fs +local hud_notif = i3.hud_notif +local POLL_FREQ = 0.25 + +IMPORT("reg_items", "reg_nodes") +IMPORT("fmt", "search", "table_merge", "array_diff") +IMPORT("is_group", "extract_groups", "item_has_groups", "apply_recipe_filters") + +local function get_filtered_items(player, data) + local items, known = {}, 0 + + for i = 1, #i3.init_items do + local item = i3.init_items[i] + local recipes = i3.recipes_cache[item] + local usages = i3.usages_cache[item] + + recipes = #apply_recipe_filters(recipes or {}, player) + usages = #apply_recipe_filters(usages or {}, player) + + if recipes > 0 or usages > 0 then + items[item] = true + known += recipes + usages + end + end + + data.known_recipes = known + + return items +end + +local function item_in_inv(item, inv_items) + local inv_items_size = #inv_items + + if is_group(item) then + local groupname = item:sub(7) + local group_cache = i3.groups[groupname] + local groups = group_cache and group_cache.groups or extract_groups(item) + + for i = 1, inv_items_size do + local def = core.registered_items[inv_items[i]] + + if def then + if item_has_groups(def.groups, groups) then + return true + end + end + end + else + for i = 1, inv_items_size do + if inv_items[i] == item then + return true + end + end + end +end + +local function recipe_in_inv(rcp, inv_items) + for _, item in pairs(rcp.items) do + if not item_in_inv(item, inv_items) then return end + end + + return true +end + +local function progressive_filter(recipes, player) + if not recipes then + return {} + end + + local name = player:get_player_name() + local data = i3.data[name] + + if #data.inv_items == 0 then + return {} + end + + local filtered, c = {}, 0 + + for i = 1, #recipes do + local recipe = recipes[i] + if recipe_in_inv(recipe, data.inv_items) then + c++ + filtered[c] = recipe + end + end + + return filtered +end + +local item_lists = {"main", "craft", "craftpreview"} + +local function get_inv_items(player) + local inv = player:get_inventory() + if not inv then + return {} + end + + local stacks = {} + + for i = 1, #item_lists do + local list = inv:get_list(item_lists[i]) + table_merge(stacks, list) + end + + local inv_items, c = {}, 0 + + for i = 1, #stacks do + local stack = stacks[i] + + if not stack:is_empty() then + local name = stack:get_name() + if core.registered_items[name] then + c++ + inv_items[c] = name + end + end + end + + return inv_items +end + +-- Workaround. Need an engine call to detect when the contents of +-- the player inventory changed, instead. +local function poll_new_items(player, data, join) + local inv_items = get_inv_items(player) + local diff = array_diff(inv_items, data.inv_items) + + if join or #diff > 0 then + data.inv_items = table_merge(diff, data.inv_items) + local oldknown = data.known_recipes or 0 + local items = get_filtered_items(player, data) + data.discovered = data.known_recipes - oldknown + + if data.discovered > 0 then + local msg = fmt("%u new recipe%s unlocked!", data.discovered, data.discovered > 1 and "s" or "") + local last_discovered = diff[1] + local img = reg_items[last_discovered].inventory_image + + if reg_nodes[last_discovered] then + local id = core.get_content_id(last_discovered) + img = i3.cubes[id] or img + end + + hud_notif(data.player_name, msg, img) + end + + data.items_progress = items + data.itab = 1 + + search(data) + set_fs(player) + end + + core.after(POLL_FREQ, poll_new_items, player, data) +end + +i3.add_recipe_filter("Default progressive filter", progressive_filter) + +core.register_on_joinplayer(function(player) + local name = player:get_player_name() + local data = i3.data[name] + if not data then return end + + data.inv_items = data.inv_items or {} + data.known_recipes = data.known_recipes or 0 + data.discovered = data.discovered or 0 + + poll_new_items(player, data, true) +end) diff --git a/mods/i3/src/styles.lua b/mods/i3/src/styles.lua new file mode 100644 index 0000000..9b36930 --- /dev/null +++ b/mods/i3/src/styles.lua @@ -0,0 +1,146 @@ +local fmt = string.format + +local PNG = { + blank = "i3_blank.png", + bg = "i3_bg.png", + bg_full = "i3_bg_full.png", + bg_goto = "i3_bg_goto.png", + bg_content = "i3_bg_content.png", + bar = "i3_bar.png", + hotbar = "i3_hotbar.png", + highlight = "i3_highlight.png", + search = "i3_search.png", + heart = "i3_heart.png", + heart_half = "i3_heart_half.png", + prev = "i3_next.png^\\[transformFX", + next = "i3_next.png", + arrow = "i3_arrow.png", + arrow_content = "i3_arrow_content.png", + trash = "i3_trash.png", + sort = "i3_sort.png", + settings = "i3_settings.png", + compress = "i3_compress.png", + fire = "i3_fire.png", + fire_anim = "i3_fire_anim.png", + book = "i3_book.png", + sign = "i3_sign.png", + cancel = "i3_cancel.png", + crafting = "i3_crafting.png", + slot = "i3_slot.png^\\[resize:128x128", + pagenum_hover = "i3_slot.png^\\[resize:128x128^\\[opacity:130", + tab = "i3_tab.png", + tab_small = "i3_tab_small.png", + tab_top = "i3_tab.png^\\[transformFY", + furnace_anim = "i3_furnace_anim.png", + shapeless = "i3_shapeless.png", + bag = "i3_bag.png", + armor = "i3_armor.png", + awards = "i3_award.png", + skins = "i3_skin.png", + waypoints = "i3_waypoint.png", + add = "i3_add.png", + refresh = "i3_refresh.png", + visible = "i3_visible.png^\\[brighten", + nonvisible = "i3_non_visible.png", + exit = "i3_exit.png", + home = "i3_home.png", + flag = "i3_flag_anim.png", + edit = "i3_edit.png", + no_result = "i3_no_result.png", + find_more = "i3_find_more.png", + search_outline = "i3_search_outline.png", + search_outline_trim = "i3_search_outline_trim.png", + all = "i3_all.png", + node = "i3_node.png", + item = "i3_item.png", + cube = "i3_cube.png", + + cancel_hover = "i3_cancel.png^\\[brighten", + search_hover = "i3_search.png^\\[brighten", + crafting_hover = "i3_crafting.png^\\[brighten", + trash_hover = "i3_trash.png^\\[brighten^\\[colorize:#f00:100", + compress_hover = "i3_compress.png^\\[brighten", + sort_hover = "i3_sort.png^\\[brighten", + settings_hover = "i3_settings.png^\\[brighten", + prev_hover = "i3_next_hover.png^\\[transformFX", + next_hover = "i3_next_hover.png", + tab_hover = "i3_tab_hover.png", + tab_small_hover = "i3_tab_small_hover.png", + tab_hover_top = "i3_tab_hover.png^\\[transformFY", + bag_hover = "i3_bag_hover.png", + armor_hover = "i3_armor_hover.png", + awards_hover = "i3_award_hover.png", + skins_hover = "i3_skin_hover.png", + waypoints_hover = "i3_waypoint_hover.png", + add_hover = "i3_add.png^\\[brighten", + refresh_hover = "i3_refresh.png^\\[brighten", + exit_hover = "i3_exit.png^\\[brighten", + home_hover = "i3_home.png^\\[brighten", + edit_hover = "i3_edit.png^\\[brighten", + all_hover = "i3_all_on.png^\\[brighten", + node_hover = "i3_node_on.png^\\[brighten", + item_hover = "i3_item_on.png^\\[brighten", +} + +local styles = string.format([[ + style_type[field;border=false;bgcolor=transparent] + style_type[label,field;font_size=16] + style_type[button;border=false;content_offset=0] + style_type[image_button,item_image_button,checkbox,dropdown;border=false;sound=i3_click] + style_type[item_image_button;bgimg_middle=9;padding=-9] + style_type[item_image_button:hovered;bgimg=%s] + + style[;sound=] + style[nofav;sound=i3_cannot] + style[search;content_offset=0] + style[pagenum,no_item,no_rcp;font=bold;font_size=18] + style[enable_search:hovered;bgimg=%s] + style[exit;fgimg=%s;fgimg_hovered=%s;content_offset=0] + style[cancel;fgimg=%s;fgimg_hovered=%s;content_offset=0] + style[prev_page,prev_recipe,prev_usage,prev_sort,prev_skin;fgimg=%s;fgimg_hovered=%s] + style[next_page,next_recipe,next_usage,next_sort,next_skin;fgimg=%s;fgimg_hovered=%s] + style[waypoint_add;fgimg=%s;fgimg_hovered=%s;content_offset=0] + style[bag_rename;fgimg=%s;fgimg_hovered=%s;content_offset=0] + style[btn_bag,btn_armor,btn_skins;font=bold;font_size=18;content_offset=0;sound=i3_click] + style[craft_rcp,craft_usg;noclip=true;font_size=16;sound=i3_craft; + bgimg=i3_btn9.png;bgimg_hovered=i3_btn9_hovered.png; + bgimg_pressed=i3_btn9_pressed.png;bgimg_middle=4,6] + style[confirm_trash_yes,confirm_trash_no,set_home;noclip=true;font_size=16; + bgimg=i3_btn9.png;bgimg_hovered=i3_btn9_hovered.png; + bgimg_pressed=i3_btn9_pressed.png;bgimg_middle=4,6] + style[confirm_trash_yes;sound=i3_trash] +]], +PNG.slot, +PNG.search_outline, +PNG.exit, PNG.exit_hover, +PNG.cancel, PNG.cancel_hover, +PNG.prev, PNG.prev_hover, +PNG.next, PNG.next_hover, +PNG.add, PNG.add_hover, +PNG.edit, PNG.edit_hover) + +local fs_elements = { + label = "label[%f,%f;%s]", + box = "box[%f,%f;%f,%f;%s]", + image = "image[%f,%f;%f,%f;%s]", + tooltip = "tooltip[%f,%f;%f,%f;%s]", + button = "button[%f,%f;%f,%f;%s;%s]", + checkbox = "checkbox[%f,%f;%s;%s;%s]", + slot = "image[%f,%f;%f,%f;" .. fmt("%s;9]", PNG.slot), + item_image = "item_image[%f,%f;%f,%f;%s]", + hypertext = "hypertext[%f,%f;%f,%f;%s;%s]", + bg9 = "background9[%f,%f;%f,%f;%s;false;12]", + scrollbar = "scrollbar[%f,%f;%f,%f;%s;%s;%u]", + model = "model[%f,%f;%f,%f;%s;%s;%s;%s;%s;%s;%s]", + image_button = "image_button[%f,%f;%f,%f;%s;%s;%s]", + animated_image = "animated_image[%f,%f;%f,%f;;%s;%u;%u]", + item_image_button = "item_image_button[%f,%f;%f,%f;%s;%s;%s]", +} + +local colors = { + yellow = "#ffd866", + black = "#2d2a2e", + blue = "#7bf", +} + +return PNG, styles, fs_elements, colors diff --git a/mods/i3/tests/test_compression.lua b/mods/i3/tests/test_compression.lua new file mode 100644 index 0000000..7e5bb73 --- /dev/null +++ b/mods/i3/tests/test_compression.lua @@ -0,0 +1,4 @@ +i3.compress("default:diamondblock", { + replace = "diamond", + by = {"bronze", "copper", "gold", "steel", "tin"} +}) diff --git a/mods/i3/tests/test_custom_recipes.lua b/mods/i3/tests/test_custom_recipes.lua new file mode 100644 index 0000000..acac746 --- /dev/null +++ b/mods/i3/tests/test_custom_recipes.lua @@ -0,0 +1,338 @@ +local mt = ItemStack("default:wood") +mt:get_meta():set_string("description", "test wood") +mt:get_meta():set_string("color", "green") + +local mt2 = ItemStack("dye:red") +mt2:get_meta():set_string("description", "test red") +mt2:get_meta():set_string("color", "#ff0") + +local mt3 = ItemStack("default:pick_diamond") +mt3:get_meta():set_string("description", "Worn Pick") +mt3:get_meta():set_string("color", "yellow") +mt3:set_wear(10000) + +minetest.register_craft { + output = mt:to_string(), + type = "shapeless", + recipe = { + "default:wood", + mt2:to_string(), + }, +} + +minetest.register_craft { + output = mt3:to_string(), + type = "shapeless", + recipe = { + "default:pick_mese", + "default:diamond", + }, +} + +minetest.clear_craft { + recipe = { + {"default:sand", "default:sand"}, + {"default:sand", "default:sand"}, + }, +} + +i3.register_craft { + url = "https://raw.githubusercontent.com/minetest-mods/i3/main/tests/test_online_recipe.json" +} + +i3.register_craft { + result = "default:ladder_wood 2", + items = {"default:copper_ingot 7, default:tin_ingot, default:steel_ingot 2"}, +} + +i3.register_craft { + result = "default:tree", + items = { + "default:wood", + "", + "default:wood" + }, +} + +i3.register_craft { + result = "default:cobble 16", + items = { + "default:stone, default:stone", + "default:stone, , default:stone", + ", default:stone, default:stone", + } +} + +i3.register_craft { + grid = { + "X", + "#", + "X", + "X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X", + "#X", + "X", + "X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + + +i3.register_craft { + grid = { + "X#", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X#X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X#XX", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X#XX", + "X#X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X#XX", + "X#X", + "#", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X##XX", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X##X#X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X##X#X", + "", + "X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X #", + " ## ", + "X#X#", + "X X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass 2", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X #", + " ## ", + "X#X#X", + "X X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X #", + " ## ", + "X#X#", + "#X#X#", + "X X", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X #", + " ## ", + "X#X#", + "#X#X#", + "X X##", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass", + }, + result = "default:mese 3", +} + + +i3.register_craft { + grid = { + "X #", + " ## ", + "X#X#", + "#X#X#", + "X X##", + " ## ", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X #", + " ## ", + "X#X#", + "#X#X#", + "X X##X", + " ## ", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X #", + " ## ", + "X#X#", + "#X#X#", + "X X##X#", + " ## ", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X #", + " ## ", + "X#X#", + "#X#X#", + "X X##X#X", + " ## ", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass", + }, + result = "default:mese 3", +} + +i3.register_craft { + grid = { + "X #", + " ## ", + "X#X#", + "#X#X#", + "X X##X#X", + " ## ", + "#X#X#", + "#X#X#", + }, + key = { + ['#'] = "default:wood", + ['X'] = "default:glass", + }, + result = "default:mese 3", +} diff --git a/mods/i3/tests/test_online_recipe.json b/mods/i3/tests/test_online_recipe.json new file mode 100644 index 0000000..aa08e59 --- /dev/null +++ b/mods/i3/tests/test_online_recipe.json @@ -0,0 +1,8 @@ +{ + "items": [ + "default:stone, default:stone, default:stone", + "default:stone, , default:stone", + "default:stone, default:stone, default:stone" + ], + "result": "default:cobble 16" +} diff --git a/mods/i3/tests/test_operators.lua b/mods/i3/tests/test_operators.lua new file mode 100644 index 0000000..aeffc55 --- /dev/null +++ b/mods/i3/tests/test_operators.lua @@ -0,0 +1,26 @@ +local a, b, c = 0, 0, 0 +b+=1 +c++; local foo = "bar"; +local t = { + a = a++, + b = 2, + c = c+=2, + d = a&3, + e = 1, +} +t["b"] <<= 4 +t.b >>= 2 +assert(t.b == 8) +--print(dump(t)) +--c += 1 +c*=2 +local i = 16 +i += i<<4 +assert(i == 272) +assert((a+=2) == 2) +assert(c++ == 3) +assert((a-=1) == -1) +assert((c^=4) == 16) +assert((a&b) == 0) +assert((c|=a) == 2) +assert((1<<8) == 256) diff --git a/mods/i3/tests/test_tabs.lua b/mods/i3/tests/test_tabs.lua new file mode 100644 index 0000000..376bfd9 --- /dev/null +++ b/mods/i3/tests/test_tabs.lua @@ -0,0 +1,34 @@ +i3.new_tab("test1", { + description = "Test 1 Test 1", + image = "i3_heart.png", + + formspec = function(player, data, fs) + fs("label", 3, 1, "Just a test") + fs"label[3,2;Lorem Ipsum]" + end, +}) + +i3.new_tab("test2", { + description = "Test 2", + image = "i3_mesepick.png", + + formspec = function(player, data, fs) + fs("label[3,1;Test 2]") + end, +}) + +i3.new_tab("test_creative", { + description = "Test creative", + + access = function(player, data) + local name = player:get_player_name() + return core.is_creative_enabled(name) + end, + + formspec = function(player, data, fs) + fs("label[3,1;Creative enabled]") + end, + + fields = i3.set_fs, +}) + diff --git a/mods/i3/textures/i3_add.png b/mods/i3/textures/i3_add.png new file mode 100644 index 0000000..68cd3ea Binary files /dev/null and b/mods/i3/textures/i3_add.png differ diff --git a/mods/i3/textures/i3_all.png b/mods/i3/textures/i3_all.png new file mode 100644 index 0000000..c404271 Binary files /dev/null and b/mods/i3/textures/i3_all.png differ diff --git a/mods/i3/textures/i3_all_on.png b/mods/i3/textures/i3_all_on.png new file mode 100644 index 0000000..b2d9972 Binary files /dev/null and b/mods/i3/textures/i3_all_on.png differ diff --git a/mods/i3/textures/i3_armor.png b/mods/i3/textures/i3_armor.png new file mode 100644 index 0000000..f838673 Binary files /dev/null and b/mods/i3/textures/i3_armor.png differ diff --git a/mods/i3/textures/i3_armor_1.png b/mods/i3/textures/i3_armor_1.png new file mode 100644 index 0000000..af0408b Binary files /dev/null and b/mods/i3/textures/i3_armor_1.png differ diff --git a/mods/i3/textures/i3_armor_2.png b/mods/i3/textures/i3_armor_2.png new file mode 100644 index 0000000..4aeb3ce Binary files /dev/null and b/mods/i3/textures/i3_armor_2.png differ diff --git a/mods/i3/textures/i3_armor_3.png b/mods/i3/textures/i3_armor_3.png new file mode 100644 index 0000000..e80c80b Binary files /dev/null and b/mods/i3/textures/i3_armor_3.png differ diff --git a/mods/i3/textures/i3_armor_4.png b/mods/i3/textures/i3_armor_4.png new file mode 100644 index 0000000..f08dd7c Binary files /dev/null and b/mods/i3/textures/i3_armor_4.png differ diff --git a/mods/i3/textures/i3_armor_5.png b/mods/i3/textures/i3_armor_5.png new file mode 100644 index 0000000..413edb6 Binary files /dev/null and b/mods/i3/textures/i3_armor_5.png differ diff --git a/mods/i3/textures/i3_armor_hover.png b/mods/i3/textures/i3_armor_hover.png new file mode 100644 index 0000000..4d94503 Binary files /dev/null and b/mods/i3/textures/i3_armor_hover.png differ diff --git a/mods/i3/textures/i3_arrow.png b/mods/i3/textures/i3_arrow.png new file mode 100644 index 0000000..42bba58 Binary files /dev/null and b/mods/i3/textures/i3_arrow.png differ diff --git a/mods/i3/textures/i3_arrow_content.png b/mods/i3/textures/i3_arrow_content.png new file mode 100644 index 0000000..f7fccd5 Binary files /dev/null and b/mods/i3/textures/i3_arrow_content.png differ diff --git a/mods/i3/textures/i3_award.png b/mods/i3/textures/i3_award.png new file mode 100644 index 0000000..680af5e Binary files /dev/null and b/mods/i3/textures/i3_award.png differ diff --git a/mods/i3/textures/i3_award_hover.png b/mods/i3/textures/i3_award_hover.png new file mode 100644 index 0000000..43e8725 Binary files /dev/null and b/mods/i3/textures/i3_award_hover.png differ diff --git a/mods/i3/textures/i3_bag.png b/mods/i3/textures/i3_bag.png new file mode 100644 index 0000000..7f0a9f1 Binary files /dev/null and b/mods/i3/textures/i3_bag.png differ diff --git a/mods/i3/textures/i3_bag_hover.png b/mods/i3/textures/i3_bag_hover.png new file mode 100644 index 0000000..cccf13b Binary files /dev/null and b/mods/i3/textures/i3_bag_hover.png differ diff --git a/mods/i3/textures/i3_bag_large.png b/mods/i3/textures/i3_bag_large.png new file mode 100644 index 0000000..4946647 Binary files /dev/null and b/mods/i3/textures/i3_bag_large.png differ diff --git a/mods/i3/textures/i3_bag_medium.png b/mods/i3/textures/i3_bag_medium.png new file mode 100644 index 0000000..e752801 Binary files /dev/null and b/mods/i3/textures/i3_bag_medium.png differ diff --git a/mods/i3/textures/i3_bag_small.png b/mods/i3/textures/i3_bag_small.png new file mode 100644 index 0000000..c9f5136 Binary files /dev/null and b/mods/i3/textures/i3_bag_small.png differ diff --git a/mods/i3/textures/i3_bar.png b/mods/i3/textures/i3_bar.png new file mode 100644 index 0000000..164b256 Binary files /dev/null and b/mods/i3/textures/i3_bar.png differ diff --git a/mods/i3/textures/i3_bg.png b/mods/i3/textures/i3_bg.png new file mode 100644 index 0000000..1cad26d Binary files /dev/null and b/mods/i3/textures/i3_bg.png differ diff --git a/mods/i3/textures/i3_bg_content.png b/mods/i3/textures/i3_bg_content.png new file mode 100644 index 0000000..d223abb Binary files /dev/null and b/mods/i3/textures/i3_bg_content.png differ diff --git a/mods/i3/textures/i3_bg_full.png b/mods/i3/textures/i3_bg_full.png new file mode 100644 index 0000000..9f86d72 Binary files /dev/null and b/mods/i3/textures/i3_bg_full.png differ diff --git a/mods/i3/textures/i3_bg_goto.png b/mods/i3/textures/i3_bg_goto.png new file mode 100644 index 0000000..6f69b91 Binary files /dev/null and b/mods/i3/textures/i3_bg_goto.png differ diff --git a/mods/i3/textures/i3_blank.png b/mods/i3/textures/i3_blank.png new file mode 100644 index 0000000..2ae3f9a Binary files /dev/null and b/mods/i3/textures/i3_blank.png differ diff --git a/mods/i3/textures/i3_btn9.png b/mods/i3/textures/i3_btn9.png new file mode 100644 index 0000000..34433ac Binary files /dev/null and b/mods/i3/textures/i3_btn9.png differ diff --git a/mods/i3/textures/i3_btn9_hovered.png b/mods/i3/textures/i3_btn9_hovered.png new file mode 100644 index 0000000..01c2dc7 Binary files /dev/null and b/mods/i3/textures/i3_btn9_hovered.png differ diff --git a/mods/i3/textures/i3_btn9_pressed.png b/mods/i3/textures/i3_btn9_pressed.png new file mode 100644 index 0000000..0cbac75 Binary files /dev/null and b/mods/i3/textures/i3_btn9_pressed.png differ diff --git a/mods/i3/textures/i3_cancel.png b/mods/i3/textures/i3_cancel.png new file mode 100644 index 0000000..c8ef35b Binary files /dev/null and b/mods/i3/textures/i3_cancel.png differ diff --git a/mods/i3/textures/i3_compress.png b/mods/i3/textures/i3_compress.png new file mode 100644 index 0000000..1584131 Binary files /dev/null and b/mods/i3/textures/i3_compress.png differ diff --git a/mods/i3/textures/i3_crafting.png b/mods/i3/textures/i3_crafting.png new file mode 100644 index 0000000..06d44e7 Binary files /dev/null and b/mods/i3/textures/i3_crafting.png differ diff --git a/mods/i3/textures/i3_cube.png b/mods/i3/textures/i3_cube.png new file mode 100644 index 0000000..3f3d27a Binary files /dev/null and b/mods/i3/textures/i3_cube.png differ diff --git a/mods/i3/textures/i3_edit.png b/mods/i3/textures/i3_edit.png new file mode 100644 index 0000000..78abdec Binary files /dev/null and b/mods/i3/textures/i3_edit.png differ diff --git a/mods/i3/textures/i3_exit.png b/mods/i3/textures/i3_exit.png new file mode 100644 index 0000000..e3fbd6d Binary files /dev/null and b/mods/i3/textures/i3_exit.png differ diff --git a/mods/i3/textures/i3_fav.png b/mods/i3/textures/i3_fav.png new file mode 100644 index 0000000..f4ddfa2 Binary files /dev/null and b/mods/i3/textures/i3_fav.png differ diff --git a/mods/i3/textures/i3_fav_off.png b/mods/i3/textures/i3_fav_off.png new file mode 100644 index 0000000..e0c36cd Binary files /dev/null and b/mods/i3/textures/i3_fav_off.png differ diff --git a/mods/i3/textures/i3_find_more.png b/mods/i3/textures/i3_find_more.png new file mode 100644 index 0000000..e4ddbe9 Binary files /dev/null and b/mods/i3/textures/i3_find_more.png differ diff --git a/mods/i3/textures/i3_fire_anim.png b/mods/i3/textures/i3_fire_anim.png new file mode 100644 index 0000000..1bc7fd3 Binary files /dev/null and b/mods/i3/textures/i3_fire_anim.png differ diff --git a/mods/i3/textures/i3_flag_anim.png b/mods/i3/textures/i3_flag_anim.png new file mode 100644 index 0000000..4cc74a7 Binary files /dev/null and b/mods/i3/textures/i3_flag_anim.png differ diff --git a/mods/i3/textures/i3_furnace_anim.png b/mods/i3/textures/i3_furnace_anim.png new file mode 100644 index 0000000..ab3bddc Binary files /dev/null and b/mods/i3/textures/i3_furnace_anim.png differ diff --git a/mods/i3/textures/i3_heart.png b/mods/i3/textures/i3_heart.png new file mode 100644 index 0000000..75d9a1e Binary files /dev/null and b/mods/i3/textures/i3_heart.png differ diff --git a/mods/i3/textures/i3_heart_half.png b/mods/i3/textures/i3_heart_half.png new file mode 100644 index 0000000..8a8e178 Binary files /dev/null and b/mods/i3/textures/i3_heart_half.png differ diff --git a/mods/i3/textures/i3_highlight.png b/mods/i3/textures/i3_highlight.png new file mode 100644 index 0000000..c55dc72 Binary files /dev/null and b/mods/i3/textures/i3_highlight.png differ diff --git a/mods/i3/textures/i3_home.png b/mods/i3/textures/i3_home.png new file mode 100644 index 0000000..bde85be Binary files /dev/null and b/mods/i3/textures/i3_home.png differ diff --git a/mods/i3/textures/i3_hotbar.png b/mods/i3/textures/i3_hotbar.png new file mode 100644 index 0000000..d7e5027 Binary files /dev/null and b/mods/i3/textures/i3_hotbar.png differ diff --git a/mods/i3/textures/i3_item.png b/mods/i3/textures/i3_item.png new file mode 100644 index 0000000..d5b50da Binary files /dev/null and b/mods/i3/textures/i3_item.png differ diff --git a/mods/i3/textures/i3_item_on.png b/mods/i3/textures/i3_item_on.png new file mode 100644 index 0000000..41dec85 Binary files /dev/null and b/mods/i3/textures/i3_item_on.png differ diff --git a/mods/i3/textures/i3_mesepick.png b/mods/i3/textures/i3_mesepick.png new file mode 100644 index 0000000..ae52c79 Binary files /dev/null and b/mods/i3/textures/i3_mesepick.png differ diff --git a/mods/i3/textures/i3_next.png b/mods/i3/textures/i3_next.png new file mode 100644 index 0000000..50dc3aa Binary files /dev/null and b/mods/i3/textures/i3_next.png differ diff --git a/mods/i3/textures/i3_next_hover.png b/mods/i3/textures/i3_next_hover.png new file mode 100644 index 0000000..057e047 Binary files /dev/null and b/mods/i3/textures/i3_next_hover.png differ diff --git a/mods/i3/textures/i3_no_result.png b/mods/i3/textures/i3_no_result.png new file mode 100644 index 0000000..f4ce4d1 Binary files /dev/null and b/mods/i3/textures/i3_no_result.png differ diff --git a/mods/i3/textures/i3_node.png b/mods/i3/textures/i3_node.png new file mode 100644 index 0000000..b4b483a Binary files /dev/null and b/mods/i3/textures/i3_node.png differ diff --git a/mods/i3/textures/i3_node_on.png b/mods/i3/textures/i3_node_on.png new file mode 100644 index 0000000..b49c9dc Binary files /dev/null and b/mods/i3/textures/i3_node_on.png differ diff --git a/mods/i3/textures/i3_non_visible.png b/mods/i3/textures/i3_non_visible.png new file mode 100644 index 0000000..273b8d9 Binary files /dev/null and b/mods/i3/textures/i3_non_visible.png differ diff --git a/mods/i3/textures/i3_refresh.png b/mods/i3/textures/i3_refresh.png new file mode 100644 index 0000000..94f6e26 Binary files /dev/null and b/mods/i3/textures/i3_refresh.png differ diff --git a/mods/i3/textures/i3_search.png b/mods/i3/textures/i3_search.png new file mode 100644 index 0000000..75ef747 Binary files /dev/null and b/mods/i3/textures/i3_search.png differ diff --git a/mods/i3/textures/i3_search_outline.png b/mods/i3/textures/i3_search_outline.png new file mode 100644 index 0000000..a641e89 Binary files /dev/null and b/mods/i3/textures/i3_search_outline.png differ diff --git a/mods/i3/textures/i3_search_outline_trim.png b/mods/i3/textures/i3_search_outline_trim.png new file mode 100644 index 0000000..9dae236 Binary files /dev/null and b/mods/i3/textures/i3_search_outline_trim.png differ diff --git a/mods/i3/textures/i3_settings.png b/mods/i3/textures/i3_settings.png new file mode 100644 index 0000000..d5b69fb Binary files /dev/null and b/mods/i3/textures/i3_settings.png differ diff --git a/mods/i3/textures/i3_shapeless.png b/mods/i3/textures/i3_shapeless.png new file mode 100644 index 0000000..2142107 Binary files /dev/null and b/mods/i3/textures/i3_shapeless.png differ diff --git a/mods/i3/textures/i3_skin.png b/mods/i3/textures/i3_skin.png new file mode 100644 index 0000000..4ed75c3 Binary files /dev/null and b/mods/i3/textures/i3_skin.png differ diff --git a/mods/i3/textures/i3_skin_hover.png b/mods/i3/textures/i3_skin_hover.png new file mode 100644 index 0000000..2ae8bb8 Binary files /dev/null and b/mods/i3/textures/i3_skin_hover.png differ diff --git a/mods/i3/textures/i3_slot.png b/mods/i3/textures/i3_slot.png new file mode 100644 index 0000000..ed144ec Binary files /dev/null and b/mods/i3/textures/i3_slot.png differ diff --git a/mods/i3/textures/i3_sort.png b/mods/i3/textures/i3_sort.png new file mode 100644 index 0000000..29e6ed8 Binary files /dev/null and b/mods/i3/textures/i3_sort.png differ diff --git a/mods/i3/textures/i3_steelpick.png b/mods/i3/textures/i3_steelpick.png new file mode 100644 index 0000000..ba90021 Binary files /dev/null and b/mods/i3/textures/i3_steelpick.png differ diff --git a/mods/i3/textures/i3_tab.png b/mods/i3/textures/i3_tab.png new file mode 100644 index 0000000..b8c9ec3 Binary files /dev/null and b/mods/i3/textures/i3_tab.png differ diff --git a/mods/i3/textures/i3_tab_hover.png b/mods/i3/textures/i3_tab_hover.png new file mode 100644 index 0000000..2923fff Binary files /dev/null and b/mods/i3/textures/i3_tab_hover.png differ diff --git a/mods/i3/textures/i3_tab_small.png b/mods/i3/textures/i3_tab_small.png new file mode 100644 index 0000000..f8cbdcc Binary files /dev/null and b/mods/i3/textures/i3_tab_small.png differ diff --git a/mods/i3/textures/i3_tab_small_hover.png b/mods/i3/textures/i3_tab_small_hover.png new file mode 100644 index 0000000..a810329 Binary files /dev/null and b/mods/i3/textures/i3_tab_small_hover.png differ diff --git a/mods/i3/textures/i3_trash.png b/mods/i3/textures/i3_trash.png new file mode 100644 index 0000000..de6f148 Binary files /dev/null and b/mods/i3/textures/i3_trash.png differ diff --git a/mods/i3/textures/i3_visible.png b/mods/i3/textures/i3_visible.png new file mode 100644 index 0000000..4d037b0 Binary files /dev/null and b/mods/i3/textures/i3_visible.png differ diff --git a/mods/i3/textures/i3_waypoint.png b/mods/i3/textures/i3_waypoint.png new file mode 100644 index 0000000..14f1e12 Binary files /dev/null and b/mods/i3/textures/i3_waypoint.png differ diff --git a/mods/i3/textures/i3_waypoint_hover.png b/mods/i3/textures/i3_waypoint_hover.png new file mode 100644 index 0000000..a58228e Binary files /dev/null and b/mods/i3/textures/i3_waypoint_hover.png differ diff --git a/mods/i3/util/luacheck.lua b/mods/i3/util/luacheck.lua new file mode 100644 index 0000000..289bd5d --- /dev/null +++ b/mods/i3/util/luacheck.lua @@ -0,0 +1,141 @@ +local exec = os.execute +local fmt, find, sub = string.format, string.find, string.sub +local var = "[%w%.%[%]\"\'_]" +local _,_, fs_elements = dofile("../src/styles.lua") + +exec "clear" + +local function split(str, delim, include_empty, max_splits, sep_is_pattern) + delim = delim or "," + max_splits = max_splits or -2 + local items = {} + local pos, len = 1, #str + local plain = not sep_is_pattern + max_splits = max_splits + 1 + repeat + local np, npe = find(str, delim, pos, plain) + np, npe = (np or (len+1)), (npe or (len+1)) + if (not np) or (max_splits == 1) then + np = len + 1 + npe = np + end + local s = sub(str, pos, np - 1) + if include_empty or (s ~= "") then + max_splits = max_splits - 1 + items[#items + 1] = s + end + pos = npe + 1 + until (max_splits == 0) or (pos > (len + 1)) + return items +end + +local files = { + "api", + "bags", + "caches", + "callbacks", + "common", + "compression", + "detached_inv", + "fields", + "groups", + "gui", + "hud", + "model_aliases", + "progressive", + "styles", +} + +local operators = { + ["([%+%-%*%^/&|])="] = function(a, b, c) + return fmt("%s = %s %s %s", a, a, b, c) + end, + + ["+%+"] = function(a, b) + return fmt("%s = %s + 1\n%s", a, a, b) + end, + + ["&"] = function(a, b) + return fmt("bit.band(%s, %s)", a, b) + end, + + ["|"] = function(a, b) + return fmt("bit.bor(%s, %s)", a, b) + end, + + ["<<"] = function(a, b) + return fmt("bit.lshift(%s, %s)", a, b) + end, + + [">>"] = function(a, b) + return fmt("bit.rshift(%s, %s)", a, b) + end, + + ["<<="] = function(a, b) + return fmt("%s = bit.lshift(%s, %s)", a, a, b) + end, + + [">>="] = function(a, b) + return fmt("%s = bit.rshift(%s, %s)", a, a, b) + end, +} + +local function compile(data) + data = data:gsub("IMPORT%((.-)%)", function(a) + return "local " .. a:gsub("\"", "") .. " = i3.get(" .. a .. ")" + end) + + data = data:gsub("([%w_]+)%(", function(a) + if fs_elements[a] then + return fmt("fs('%s',", a) + end + end) + + data = data:gsub("([%w_]+)-%-\n", function(a) + return fmt("%s = %s - 1", a, a) + end) + + for op, func in pairs(operators) do + data = data:gsub("(" .. var .. "+)%s?" .. op .. "%s?(" .. var .. "*)", func) + end + + return data +end + +for _, p in ipairs(files) do + local function _load(path, line, data, t) + if line then + if not t then + t = split(data, "\n") + end + t[line] = t[line]:gsub("(" .. var .. "+)%s?=%s?(" .. var .. "*)", "%2") + data = table.concat(t, "\n") + else + local file = assert(io.open(path, "r")) + data = file:read"*a" + file:close() + data = compile(data) + end + + local l, err = loadstring(data) + + if not l then + local err_line = tonumber(err:match(":(%d+):")) + + if line ~= err_line then + return _load(path, err_line, data, t) + end + end + + local _file = io.open(path:match("(.*)%.") .. ".lc", "w") + _file:write(data) + _file:close() + end + + _load("../src/" .. p .. ".lua") +end + +exec "luacheck ../init.lua" +exec "luacheck ../src/preprocessor.lua" +exec "luacheck ../src/*.lc" +exec "rm ../src/*.lc" diff --git a/mods/i3/util/optipng.sh b/mods/i3/util/optipng.sh new file mode 100644 index 0000000..4e59b44 --- /dev/null +++ b/mods/i3/util/optipng.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Colors with 0 alpha need to be preserved, because opaque leaves ignore alpha. +# For that purpose, the use of indexed colors is disabled (-nc). + +cd ../textures +find -name '*.png' -print0 | xargs -0 optipng -o7 -zm1-9 -nc -strip all -clobber diff --git a/mods/i3/util/servers.lua b/mods/i3/util/servers.lua new file mode 100644 index 0000000..abd06a7 --- /dev/null +++ b/mods/i3/util/servers.lua @@ -0,0 +1,23 @@ +local JSON = require"JSON" -- luarocks install json-lua +os.execute "clear" + +local list = io.popen("curl -s -H 'Accept: text/html' http://servers.minetest.net/list"):read("*a") + list = JSON:decode(list).list + +local servers = {} + +for _, server in ipairs(list) do + if server.mods then + for _, mod in ipairs(server.mods) do + if mod == "i3" then + table.insert(servers, server.name) + end + end + end +end + +if #servers > 0 then + print(("=> %u/%u servers using [i3]:\n\t• %s"):format(#servers, #list, table.concat(servers, "\n\t• "))) +else + print"No server using [i3]" +end