Compare commits

..

102 Commits
1.3 ... 1.5

Author SHA1 Message Date
b5bb00b90c Update Screenshot 2021-11-30 19:27:43 +01:00
ab690398b7 Add script for Luacheck 2021-11-30 19:00:47 +01:00
0b4250b54b Small cleaning 2021-11-30 16:55:32 +01:00
b0ed0f8497 Some cleanup 2021-11-30 05:19:17 +01:00
80b675f817 Last commit's cleanup 2021-11-30 05:05:07 +01:00
95b0434f95 Bags can have custom names 2021-11-30 02:30:49 +01:00
94a86fc0c4 Add comment 2021-11-29 20:41:33 +01:00
d459521b67 Small cleaning 2021-11-29 20:24:26 +01:00
939019f4c6 Don't allow putting a filled bag in another bag 2021-11-29 20:03:30 +01:00
27226a0d1b Reduce code + Remove github workflow 2021-11-29 19:22:42 +01:00
91af3b73d2 More cleaning 2021-11-29 15:51:12 +01:00
b92857b42a Support for plantlike in isometric map preview 2021-11-29 04:36:37 +01:00
eaf7c486b8 Add custom operators (Part 2) 2021-11-29 02:48:25 +01:00
34548d8509 Add custom operators (Part 1) 2021-11-29 00:35:50 +01:00
b3f1cf255d Move stuff to HUD 2021-11-26 05:02:08 +01:00
54ba95ac80 API: Add i3.hud_notif 2021-11-26 03:34:01 +01:00
40a8c548ab Make correct inventory cubes 2021-11-25 19:31:27 +01:00
eb5a0a68de Add an isometric area preview on Waypoints 2021-11-25 05:04:01 +01:00
3d9881fd6c Add HUD_SPEED var 2021-11-24 17:12:20 +01:00
b0326dab43 Progressive Mode: more cleaning 2021-11-24 02:46:36 +01:00
02e66f368a Cleanup Progressive mode 2021-11-24 01:50:43 +01:00
0cedd16efe Clean bag code (warning: break backward compat) 2021-11-24 01:08:47 +01:00
88b1e56c87 Small cleaning 2021-11-23 01:47:59 +01:00
1285a77060 Some cleaning 2021-11-22 22:53:14 +01:00
3c1355b13e Re-upload a new screenshot 2021-11-21 22:47:46 +01:00
dae512015e Update Screenshot 2021-11-21 19:28:01 +01:00
ec4e929491 GUI: show wear for recipe output 2021-11-21 18:57:40 +01:00
b4d7dfddef README: Add new badges 2021-11-21 06:03:22 +01:00
a64266498f Some polish to awards fs 2021-11-20 17:12:04 +01:00
734136ac46 Minor thing 2021-11-20 16:38:04 +01:00
6c3d2fe5cf Minor API change 2021-11-19 19:35:41 +01:00
dc8efbd16c De-hardcode 2021-11-19 02:08:29 +01:00
b62687e84b Small cleanup 2021-11-16 08:50:34 +01:00
d95caf847a Little tweaking 2021-11-16 05:09:37 +01:00
8f49914c9f Minor optimizations 2021-11-16 00:23:06 +01:00
c5dec1bc77 Disable fields for no-op buttons 2021-11-15 16:50:27 +01:00
a5751a2771 Small cleanup 2021-11-15 16:30:59 +01:00
58e2eb22a7 Optimize textures 2021-11-15 11:06:25 +01:00
09b34f0274 Add tooltips to options 2021-11-15 03:29:21 +01:00
e60f986484 Minor polish 2021-11-15 01:55:48 +01:00
f2248ae176 Tweak some colors 2021-11-15 00:46:02 +01:00
849fadb674 Some polish to previous commit 2021-11-15 00:09:06 +01:00
2e7dcd714d Totally rework how bags work + other cool stuff 2021-11-14 22:16:40 +01:00
9cc8464111 Little polish 2021-11-11 04:05:13 +01:00
09cb35e1fd Allow for group bag=4 2021-11-09 01:42:17 +01:00
ea8b216fa5 Update Screenshot 2021-11-08 21:56:54 +01:00
d9f8b738e5 Cleanup waypoints code 2021-11-08 21:03:38 +01:00
2f612bb5da Small fix 2021-11-08 20:13:51 +01:00
6f5d0a1760 Inventory sorting: Add option for rejecting items 2021-11-08 20:10:52 +01:00
c51da4a20b New sounds for teleport/trash 2021-11-08 18:48:42 +01:00
af2c1304ac Small tweak 2021-11-07 23:12:09 +01:00
5587341f2e Cleaning + API fix 2021-11-07 22:55:01 +01:00
9afbee72ce Additional check in API 2021-11-06 02:20:34 +01:00
22bfbf1f56 Simplify recipes comparison 2021-11-04 01:01:33 +01:00
f779492aed Add new badge to README 2021-11-03 04:34:29 +01:00
bbe020885c Tweak .luacheck 2021-11-03 04:31:36 +01:00
fdfacba495 Create luacheck.yml 2021-11-03 04:25:58 +01:00
d6085d1c63 Rename folder 2021-11-02 02:11:08 +01:00
47e68a3e92 More cleaning 2021-11-02 01:47:13 +01:00
25e57f6a23 Minor cleaning 2021-11-01 15:43:38 +01:00
4fcf4f5790 Rename some variables 2021-11-01 15:34:10 +01:00
1728f4beac Add 'ignore hotbar' option for inventory sorting 2021-11-01 15:29:20 +01:00
9276598e3e Add new buttons to main inventory, add inventory sorting methods + API 2021-10-31 23:17:15 +01:00
8d7ca9df18 Add online recipe to tests 2021-10-31 15:03:47 +01:00
3b98f4bec7 Wording 2021-10-25 23:41:56 +02:00
980adeb71f Some cleaning 2021-10-25 22:00:19 +02:00
2d9e4ee474 Fix CRLF 2021-10-25 19:18:11 +02:00
3a83e015ae Small cleaning 2021-10-25 07:38:18 +02:00
6d2682b8d9 Cleaning 2021-10-25 06:45:50 +02:00
9e50f58f5b Add Compression API 2021-10-25 00:23:44 +02:00
bd5ea4e6a8 Split init.lua into separate files (Part II) 2021-10-24 23:31:01 +02:00
caba7f3599 Some cleaning 2021-10-19 06:54:34 +02:00
43b20b317e Add Troubleshooting to README 2021-10-19 05:17:27 +02:00
8cd04deff4 API Cleanup 2021-10-19 04:16:23 +02:00
853ddc9d94 Split init.lua into separate files (Part I) 2021-10-19 03:42:45 +02:00
f8ac9c45d1 Add i3.get_recipes() 2021-10-18 21:24:29 +02:00
b6d36e59b0 Add missing doc 2021-10-18 21:11:07 +02:00
744c9d5b02 Follow-up commit 2021-10-18 21:03:58 +02:00
e5f446480e Merge pull request #34 from wsor4035/main
Allow bags via item groups
2021-10-18 20:54:01 +02:00
a147b9e6a3 spacing 2021-10-13 20:33:06 -04:00
22961b2758 adress review 2021-10-13 20:31:21 -04:00
ffa50bc3a7 allow bags via item groups 2021-10-12 15:31:08 -04:00
815651a32f Fix custom recipes display, add i3.get_current_tab(), remove some superfluous chat messages 2021-09-19 22:04:49 +02:00
b8db525dac Hide Quick Crafting scrollbars after click 2021-08-24 11:11:38 +02:00
cbf6256593 Fix potential crash 2021-08-22 02:53:39 +02:00
893959b5d4 Fix potential crash in Progressive Mode 2021-08-21 13:27:50 +02:00
8a5a14747e Fix crash related to model preview 2021-08-11 17:46:17 +02:00
b53959f177 Item compression: small fix 2021-08-10 02:10:08 +02:00
685b40f318 Update README 2021-08-10 01:57:36 +02:00
e96d279d29 Progressive Mode: minor fix + HUD bold text support 2021-08-06 04:15:43 +02:00
c87a179cb5 Minor cleaning 2021-08-06 02:56:36 +02:00
7e2256253d Fix skin_id in skins dropdown 2021-07-06 10:58:19 +02:00
f2cc874ec0 Some tweakings 2021-07-06 01:38:16 +02:00
5c96ede065 Add confirmation dialog on inventory clearing 2021-07-05 23:38:43 +02:00
1c4ae1df28 Outdated clients are now kicked ruthlessly without fallback 2021-07-05 22:45:32 +02:00
970220c561 Add .editorconfig 2021-07-05 03:32:31 +02:00
4ca50e846d Fix potential issue with progressive mode 2021-07-05 03:23:19 +02:00
0ab3dd5ffa Fix item compression setting (fix #27) 2021-07-04 18:19:49 +02:00
7820d88a30 Rename some API functions 2021-06-30 21:54:18 +02:00
34841eddf3 Small fix for search filters 2021-06-30 21:41:38 +02:00
6f588e1927 Searches are now additive (Google-like) 2021-06-30 21:23:58 +02:00
d0ff046873 Update screenshot again 2021-06-28 00:45:57 +02:00
48 changed files with 4595 additions and 3336 deletions

9
.editorconfig Normal file
View File

@ -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

View File

@ -1,6 +1,7 @@
allow_defined_top = true allow_defined_top = true
ignore = { ignore = {
"631", -- Line is too long.
"get_debug_grid", "get_debug_grid",
} }
@ -13,6 +14,8 @@ read_globals = {
"string", "string",
"table", "table",
"ItemStack", "ItemStack",
"VoxelArea",
"VoxelManip",
} }
globals = { globals = {
@ -21,3 +24,13 @@ globals = {
"sfinv", "sfinv",
"unified_inventory", "unified_inventory",
} }
exclude_files = {
"tests/test_compression.lua",
"tests/test_custom_recipes.lua",
"tests/test_operators.lua",
"tests/test_tabs.lua",
".install",
".luarocks",
}

151
API.md
View File

@ -2,13 +2,15 @@
### Custom tabs ### Custom tabs
#### `i3.new_tab(def)` #### `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): Custom tabs can be added to the `i3` inventory as follow (example):
```Lua ```Lua
i3.new_tab { i3.new_tab("stuff", {
name = "stuff",
description = "Stuff", description = "Stuff",
image = "image.png", -- Optional, adds an image next to the tab description image = "image.png", -- Optional, adds an image next to the tab description
@ -24,24 +26,29 @@ i3.new_tab {
fs("label[3,1;This is just a test]") fs("label[3,1;This is just a test]")
end, end,
-- Events handling happens here
fields = function(player, data, fields) fields = function(player, data, fields)
end, end,
} })
``` ```
- `player` is an `ObjectRef` to the user. - `player` is an `ObjectRef` to the user.
- `data` are the user data. - `data` are the user data.
- `fs` is the formspec table which is callable with a metamethod. Each call adds a new entry. - `fs` is the formspec table which is callable with a metamethod. Each call adds a new entry.
#### `i3.set_fs(player[, extra_formspec])` #### `i3.set_fs(player)`
Updates the current formspec. `extra_formspec` adds an additional formspec string. Updates the current formspec.
#### `i3.delete_tab(tabname)` #### `i3.remove_tab(tabname)`
Deletes a tab by name. Deletes a tab by name.
#### `i3.get_current_tab(player)`
Returns the current player tab. `player` is an `ObjectRef` to the user.
#### `i3.set_tab(player[, tabname])` #### `i3.set_tab(player[, tabname])`
Sets the current tab by name. `player` is an `ObjectRef` to the user. Sets the current tab by name. `player` is an `ObjectRef` to the user.
@ -51,9 +58,9 @@ Sets the current tab by name. `player` is an `ObjectRef` to the user.
Overrides a tab by name. `def` is the tab definition like seen in `i3.set_tab`. Overrides a tab by name. `def` is the tab definition like seen in `i3.set_tab`.
#### `i3.get_tabs()` #### `i3.tabs`
Returns the list of registered tabs. A list of registered tabs.
--- ---
@ -77,28 +84,28 @@ i3.register_craft_type("digging", {
#### Registering a custom crafting recipe (examples) #### Registering a custom crafting recipe (examples)
```Lua ```Lua
i3.register_craft({ i3.register_craft {
type = "digging", type = "digging",
result = "default:cobble 2", result = "default:cobble 2",
items = {"default:stone"}, items = {"default:stone"},
}) }
``` ```
```Lua ```Lua
i3.register_craft({ i3.register_craft {
result = "default:cobble 16", result = "default:cobble 16",
items = { items = {
"default:stone, default:stone, default:stone", "default:stone, default:stone, default:stone",
"default:stone, , default:stone", "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: Recipes can be registered in a Minecraft-like way:
```Lua ```Lua
i3.register_craft({ i3.register_craft {
grid = { grid = {
"X #", "X #",
" ## ", " ## ",
@ -110,13 +117,13 @@ i3.register_craft({
['X'] = "default:glass", ['X'] = "default:glass",
}, },
result = "default:mese 3", result = "default:mese 3",
}) }
``` ```
Multiples recipes can also be registered: Multiples recipes can also be registered:
```Lua ```Lua
i3.register_craft({ i3.register_craft {
{ {
result = "default:mese", result = "default:mese",
items = { items = {
@ -134,15 +141,15 @@ i3.register_craft({
"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¹): Recipes can be registered from a given URL containing a JSON file (HTTP support is required¹):
```Lua ```Lua
i3.register_craft({ i3.register_craft {
url = "https://raw.githubusercontent.com/minetest-mods/i3/main/tests/test_online_recipe.json" url = "https://raw.githubusercontent.com/minetest-mods/i3/main/tests/test_online_recipe.json"
}) }
``` ```
--- ---
@ -178,26 +185,22 @@ end)
Removes all recipe filters and adds a new one. Removes all recipe filters and adds a new one.
#### `i3.delete_recipe_filter(name)` #### `i3.recipe_filters`
Removes the recipe filter with the given `name`. A map of recipe filters, indexed by name.
#### `i3.get_recipe_filters()`
Returns a map of recipe filters, indexed by name.
--- ---
### Search filters ### Search filters
Search filters are used to perform specific searches inside the search field. Search filters are used to perform specific searches from the search field.
These filters are cumulative to perform a specific search. The filters can be cumulated to perform a specific search.
They can be used like so: `<optional_name> +<filter name>=<value1>,<value2>,<...>` They are used like so: `<optional_name> +<filter name>=<value1>,<value2>,<...>`
Example usages: Example usages:
- `+groups=cracky,crumbly`: search for groups `cracky` and `crumbly` in all items. - `+groups=cracky,crumbly` -> search for groups `cracky` and `crumbly` in all items.
- `wood +groups=flammable`: search for group `flammable` amongst items which contain - `wood +groups=flammable` -> search for group `flammable` amongst items which contain
`wood` in their names. `wood` in their names.
Notes: Notes:
@ -206,9 +209,12 @@ Notes:
#### `i3.add_search_filter(name, function(item, values))` #### `i3.add_search_filter(name, function(item, values))`
Adds a search filter with the given `name`. `values` is a table of all possible values. Adds a search filter.
The search function must return a boolean value (whether the given item should be listed or not). 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: Example function sorting items by drawtype:
```lua ```lua
@ -225,23 +231,98 @@ i3.add_search_filter("types", function(item, drawtypes)
end) end)
``` ```
#### `i3.remove_search_filter(name)` #### `i3.search_filters`
Removes the search filter with the given `name`. A map of search filters, indexed by name.
#### `i3.get_search_filters()` ---
Returns 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)`
Adds 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)`
Adds a new group of items to compress.
- `item` is the item that serve as stereotype for 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 ### Miscellaneous
#### `i3.hud_notif(name, msg[, img])`
Shows 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)`
Returns a table of recipes and usages of `item`.
#### `i3.export_url` #### `i3.export_url`
If set, the mod will export all the cached recipes and usages in a JSON format If set, the mod will export all the cached recipes and usages in a JSON format
to the given URL (HTTP support is required¹). 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`. **¹** Add `i3` to the `secure.http_mods` or `secure.trusted_mods` setting in `minetest.conf`.

View File

@ -1,29 +1,30 @@
# i3 # i3
[![ContentDB](https://content.minetest.net/packages/jp/i3/shields/title/)](https://content.minetest.net/packages/jp/i3/) [![ContentDB](https://content.minetest.net/packages/jp/i3/shields/downloads/)](https://content.minetest.net/packages/jp/i3/) [![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)]() [![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. #### **`i3`** is a next-generation inventory for Minetest.
This inventory offers a slick, modern UI made with the latest technologies of the Minetest engine. 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. **`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.4+** This mod requires **Minetest 5.4+**
#### List of features: #### List of features:
- Crafting Guide (only in survival mode) - Crafting Guide (survival mode only)
- Progressive Mode¹ - Progressive Mode¹
- Quick Crafting - Quick Crafting
- Backpacks - 3D Player Model Real-Time Preview
- 3D Player Model Preview - Isometric Map Preview
- Inventory Sorting (alphabetical + item stack compression) - Inventory Sorting (+ options: compression, reverse mode, automation, etc.)
- Item List Compression (**`moreblocks`** is supported)
- Item Bookmarks - Item Bookmarks
- Waypoints - Waypoints
- Item List Compression (**`moreblocks`** supported) - Bags
- Home
**¹** *This mode is a Terraria-like system that shows recipes you can craft from items you ever had in your inventory. **¹** *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`.* To enable it: `i3_progressive_mode = true` in `minetest.conf`.*
#### This mod officially supports the following mods: #### This mod officially supports the following mods:
- [**`3d_armor`**](https://content.minetest.net/packages/stu/3d_armor/) - [**`3d_armor`**](https://content.minetest.net/packages/stu/3d_armor/)
- [**`skinsdb`**](https://content.minetest.net/packages/bell07/skinsdb/) - [**`skinsdb`**](https://content.minetest.net/packages/bell07/skinsdb/)
@ -31,22 +32,25 @@ To enable it: `i3_progressive_mode = true` in `minetest.conf`.*
#### Recommendations #### Recommendations
To use this mod in the best conditions, it's recommended to follow these recommendations: To use this mod in the best conditions:
- Use LuaJIT - Use LuaJIT
- Use a HiDPI widescreen display - Use a HiDPI widescreen display
- Use the default Freetype font style - 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.
#### Notes #### Notes
`i3` uses a larger inventory than the usual inventories in Minetest games. `i3` uses a larger inventory than the usual inventories in Minetest games.
Thus, most chests will be unadapted to this inventory size. Thus, most chests will be unadapted to this inventory size.
The `i3` inventory is 9 slots wide by default (without backpack), such as in Minecraft. The `i3` inventory is 9 slots wide by default, such as Minecraft.
Report any bug on the [**Bug Tracker**](https://github.com/minetest-mods/i3/issues). Report bugs on the [**Bug Tracker**](https://github.com/minetest-mods/i3/issues).
Love this mod? Donations are appreciated: https://www.paypal.me/jpg84240 **Video review on YouTube:** https://www.youtube.com/watch?v=Xd14BCdEZ3o
Demo video (outdated): https://www.youtube.com/watch?v=25nCAaqeacU ![Preview](https://user-images.githubusercontent.com/7883281/144105848-535b5e4f-a6fc-44d6-add1-e62a45d36f63.png)
![Preview](https://user-images.githubusercontent.com/7883281/123556172-f670a180-d789-11eb-9168-eb38fc764736.png)

3405
init.lua

File diff suppressed because it is too large Load Diff

129
luacheck.lua Executable file
View File

@ -0,0 +1,129 @@
local exec = os.execute
local fmt, find, sub = string.format, string.find, string.sub
local var = "[%w%.%[%]\"\'_]"
exec("reset")
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",
"compress",
"detached_inv",
"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)
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("(.*)%.") .. ".l", "w")
_file:write(data)
_file:close()
end
_load("./src/" .. p .. ".lua")
end
exec("luacheck init.lua")
exec("luacheck ./src/operators.lua")
exec("luacheck ./src/*.l")
exec("rm ./src/*.l")

BIN
sounds/i3_cannot.ogg Normal file

Binary file not shown.

0
sounds/i3_click.ogg Executable file → Normal file
View File

0
sounds/i3_craft.ogg Executable file → Normal file
View File

0
sounds/i3_tab.ogg Executable file → Normal file
View File

BIN
sounds/i3_teleport.ogg Normal file

Binary file not shown.

BIN
sounds/i3_trash.ogg Normal file

Binary file not shown.

348
src/api.lua Normal file
View File

@ -0,0 +1,348 @@
local make_fs = i3.files.gui()
IMPORT("gmatch", "split")
IMPORT("S", "err", "fmt", "reg_items")
IMPORT("sorter", "sort_inventory")
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 true_str(def.url) then
if not i3.http then
return err(fmt([[i3.register_craft(): Unable to reach %s.
No HTTP support for this mod: add it to the `secure.http_mods` or
`secure.trusted_mods` setting.]], def.url))
end
i3.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) 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, len = def.items, #def.items
def.items = {}
for i = 1, len do
local rlen = #split(items[i], ",")
if rlen > width then
width = rlen
end
end
for i = 1, len do
while #split(items[i], ",") < width do
items[i] = fmt("%s,", items[i])
end
end
for name in gmatch(concat(items, ","), "[%s%w_:]+") do
c++
def.items[c] = clean_name(name)
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)
return {
recipes = i3.recipes_cache[item],
usages = i3.usages_cache[item]
}
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
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
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, def in ipairs(i3.tabs) do
if def.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
if img then
data.hud_img = fmt("%s^[resize:16x16", 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.reverse_sorting, 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.reverse_sorting, 2)
return list
end,
})

173
src/bags.lua Normal file
View File

@ -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

331
src/caches.lua Normal file
View File

@ -0,0 +1,331 @@
local replacements = {fuel = {}}
IMPORT("maxn", "copy", "insert", "sort", "match", "sub")
IMPORT("is_group", "extract_groups", "item_has_groups", "groups_to_items")
IMPORT("fmt", "reg_items", "reg_aliases", "reg_nodes", "draw_cube", "ItemStack")
IMPORT("true_str", "true_table", "is_table", "valid_item", "table_merge", "table_replace", "rcp_eq")
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 get_item_usages(item, recipe, added)
local groups = extract_groups(item)
if groups then
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, true)
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)
if true_str(def) then
return -- TODO
elseif is_table(def) then
return -- TODO
end
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 rcp_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 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 i3.http and true_str(i3.export_url) then
local post_data = {
recipes = i3.recipes_cache,
usages = i3.usages_cache,
}
i3.http.fetch_async {
url = i3.export_url,
post_data = core.write_json(post_data),
}
end
end
local function get_cube(tiles)
if not true_table(tiles) then
return "i3_blank.png"
end
local t = copy(tiles)
local texture
for k, v in pairs(t) do
if type(v) == "table" then
t[k] = v.name
end
end
-- Tiles: up, down, right, left, back, front
-- Inventory cube: up, front, right
if #t <= 2 then
texture = draw_cube(t[1], t[1], t[1])
elseif #t <= 5 then
texture = draw_cube(t[1], t[3], t[3])
else -- Full tileset
texture = draw_cube(t[1], t[6], t[3])
end
return texture
end
local function init_cubes()
for name, def in pairs(reg_nodes) do
if def then
local id = core.get_content_id(name)
if def.drawtype == "normal" or def.drawtype == "liquid" or
sub(def.drawtype, 1, 9) == "glasslike" or
sub(def.drawtype, 1, 8) == "allfaces" then
i3.cubes[id] = get_cube(def.tiles)
elseif sub(def.drawtype, 1, 9) == "plantlike" or sub(def.drawtype, 1, 8) == "firelike" then
i3.plants[id] = def.inventory_image .. "^\\[resize:16x16"
end
end
end
end
return function()
init_recipes()
init_cubes()
end

522
src/callbacks.lua Normal file
View File

@ -0,0 +1,522 @@
local _, get_inventory_fs = i3.files.gui()
local set_fs = i3.set_fs
IMPORT("vec_eq", "vec_round")
IMPORT("reg_items", "reg_aliases")
IMPORT("sort", "copy", "insert", "remove", "indexof")
IMPORT("S", "min", "random", "translate", "ItemStack")
IMPORT("fmt", "find", "match", "sub", "lower", "split", "toupper")
IMPORT("msg", "is_fav", "pos_to_str", "str_to_pos", "add_hud_waypoint", "play_sound", "spawn_item")
IMPORT("search", "get_sorting_idx", "sort_inventory", "sort_by_category", "get_recipes", "get_detached_inv")
IMPORT("valid_item", "get_stack", "craft_stack", "clean_name", "compressible", "check_privs", "safe_teleport")
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.recipes = nil
data.usages = nil
data.export_rcp = nil
data.export_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
i3.new_tab("inventory", {
description = S"Inventory",
formspec = get_inventory_fs,
fields = function(player, data, fields)
local name = data.player_name
local inv = player:get_inventory()
local sb_inv = fields.scrbar_inv
if fields.skins then
local id = tonumber(fields.skins)
local _skins = skins.get_skinlist_for_player(name)
skins.set_player_skin(player, _skins[id])
end
if fields.drop_items then
local items = split(fields.drop_items, ",")
data.drop_items = items
end
for field in pairs(fields) do
if sub(field, 1, 4) == "btn_" then
data.subcat = indexof(i3.SUBCAT, 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
elseif sub(field, 1, 8) == "setting_" then
data.show_setting = match(field, "_(%w+)$")
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
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.prev_sort or fields.next_sort then
local idx = get_sorting_idx(data.sort)
local tot = #i3.sorting_methods
idx -= (fields.prev_sort and 1 or -1)
if idx > tot then
idx = 1
elseif idx == 0 then
idx = tot
end
data.sort = i3.sorting_methods[idx].name
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 sb_inv and sub(sb_inv, 1, 3) == "CHG" then
data.scrbar_inv = tonumber(match(sb_inv, "%d+"))
return
elseif fields.waypoint_add then
local pos = player:get_pos()
for _, v in ipairs(data.waypoints) do
if vec_eq(vec_round(pos), vec_round(str_to_pos(v.pos))) then
play_sound(name, "i3_cannot", 0.8)
return msg(name, "You already 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
end
return set_fs(player)
end,
})
local function select_item(player, data, _f)
local item
for field in pairs(_f) 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.export_rcp = nil
data.export_usg = nil
end
end
local function rcp_fields(player, data, fields)
local sb_rcp, sb_usg = fields.scrbar_rcp, fields.scrbar_usg
if fields.cancel then
reset_data(data)
elseif fields.exit then
data.query_item = nil
elseif 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.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_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.export_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.export_usg = nil
data.scrbar_usg = 1
elseif fields.fav then
local fav, i = is_fav(data.favs, data.query_item)
local total = #data.favs
if total < i3.MAX_FAVS and not fav then
data.favs[total + 1] = data.query_item
elseif fav then
remove(data.favs, i)
end
elseif fields.export_rcp or fields.export_usg then
if fields.export_rcp then
data.export_rcp = not data.export_rcp
if not data.export_rcp then
data.scrbar_rcp = 1
end
else
data.export_usg = not data.export_usg
if not data.export_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.export_rcp = nil
data.scrbar_rcp = 1
else
data.export_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"Come back when your client is up-to-date.")
elseif formname ~= "" then
return false
end
-- No-op buttons
if fields.player_name or fields.awards or fields.home_pos or fields.pagenum or
fields.no_item or fields.no_rcp or fields.select_sorting or fields.sort_method or
fields.bg_content 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)
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.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)

731
src/common.lua Normal file
View File

@ -0,0 +1,731 @@
local ItemStack = ItemStack
local loadstring = loadstring
local translate = core.get_translated_string
local vec_new, vec_add, vec_mul = vector.new, vector.add, vector.multiply
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
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 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 = core.registered_items[item]
local desc = lower(translate(data.lang_code, def and 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 rcp_eq(rcp, rcp2)
if rcp.type ~= rcp2.type then return end
if rcp.width ~= rcp2.width then return end
if #rcp.items ~= #rcp2.items then return end
if rcp.output ~= rcp2.output then return end
for i, item in pairs(rcp.items) do
if item ~= rcp2.items[i] then return end
end
for i, item in pairs(rcp2.items) do
if item ~= rcp.items[i] 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)
if sub(str, 1, 6) == "group:" then
return split(sub(str, 7), ",")
end
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 groups_to_items(groups, get_all)
if not get_all and #groups == 1 then
local group = groups[1]
local stereotype = i3.group_stereotypes[group]
local def = core.registered_items[stereotype]
if valid_item(def) then
return stereotype
end
end
local names = {}
for name, def in pairs(core.registered_items) do
if valid_item(def) and item_has_groups(def.groups, groups) then
if get_all then
insert(names, name)
else
return name
end
end
end
return get_all and names or ""
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 compression_active(data)
return i3.item_compression and not next(i3.recipe_filters) and data.filter == ""
end
local function compressible(item, data)
return compression_active(data) and i3.compress_groups[item]
end
local function is_fav(favs, query_item)
local fav, i
for j = 1, #favs do
if favs[j] == query_item then
fav = true
i = j
break
end
end
return fav, i
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 = vec_add(ppos, vec_mul(dir, 1))
core.add_item(look_at, stack)
end
local function get_recipes(player, item)
local clean_item = core.registered_aliases[item] or item
local recipes = i3.recipes_cache[clean_item]
local usages = i3.usages_cache[clean_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 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.export_counts[rcp_usg].rcp) do
local items = {[name] = count}
if is_group(name) then
items = {}
local groups = extract_groups(name)
local item_groups = groups_to_items(groups, true)
local remaining = count
for _, item in ipairs(item_groups) do
for _name, _count in pairs(data.export_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
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(vec_mul(vel, -1))
local p = vec_new(pos)
p.y += 0.25
player:set_pos(p)
end
local function get_sorting_idx(name)
local idx = 1
for i, def in ipairs(i3.sorting_methods) do
if name == def.name then
idx = i
end
end
return idx
end
local function sorter(inv, reverse, mode)
sort(inv, function(a, b)
if mode == 1 then
a, b = a:get_name(), b:get_name()
else
a, b = a:get_count(), b:get_count()
end
if reverse 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 drop_items(player, inv, list, start_i, rej)
for i = start_i, #list do
local stack = list[i]
local name = stack:get_name()
for _, it in ipairs(rej) do
if name == it then
spawn_item(player, stack)
inv:set_stack("main", i, ItemStack(""))
end
end
end
return inv:get_list"main"
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 10 or 1
if true_table(data.drop_items) then
list = drop_items(player, inv, list, start_i, data.drop_items)
end
if data.inv_compress then
list = compress_items(list, start_i)
else
list = pre_sorting(list, start_i)
end
local idx = get_sorting_idx(data.sort)
local new_inv = i3.sorting_methods[idx].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 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 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,
-- Compression
compressible = compressible,
compression_active = compression_active,
-- Sorting
search = search,
sorter = sorter,
get_recipes = get_recipes,
sort_inventory = sort_inventory,
get_sorting_idx = get_sorting_idx,
sort_by_category = sort_by_category,
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
ItemStack = ItemStack,
valid_item = valid_item,
spawn_item = spawn_item,
clean_name = clean_name,
play_sound = play_sound,
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,
-- Inventory
get_stack = get_stack,
craft_stack = craft_stack,
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,
-- 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,
rcp_eq = rcp_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_new = vector.new,
vec_add = vector.add,
vec_sub = vector.subtract,
vec_mul = vector.multiply,
vec_round = vector.round,
vec_eq = vector.equals,
}
function i3.get(...)
local t = {}
for i, var in ipairs{...} do
t[i] = _[var]
end
return newunpack(t)
end

View File

@ -1,4 +1,4 @@
local fmt, insert = string.format, table.insert IMPORT("fmt", "copy", "insert")
local wood_types = { local wood_types = {
"acacia_wood", "aspen_wood", "junglewood", "pine_wood", "acacia_wood", "aspen_wood", "junglewood", "pine_wood",
@ -44,7 +44,7 @@ local to_compress = {
["default:mese_post_light"] = { ["default:mese_post_light"] = {
replace = "mese_post_light", replace = "mese_post_light",
by = { by = {
"mese_post_light_acacia", "mese_post_light_acacia_wood",
"mese_post_light_aspen_wood", "mese_post_light_aspen_wood",
"mese_post_light_junglewood", "mese_post_light_junglewood",
"mese_post_light_pine_wood", "mese_post_light_pine_wood",
@ -262,7 +262,7 @@ local moreblocks_nodes = {
"ice", "ice",
} }
local colors_moreblocks = table.copy(colors) local colors_moreblocks = copy(colors)
insert(colors_moreblocks, "white") insert(colors_moreblocks, "white")
local moreblocks_mods = { local moreblocks_mods = {
@ -316,4 +316,4 @@ for _, v2 in ipairs(v) do
end end
end end
return compressed, _compressed i3.compress_groups, i3.compressed = compressed, _compressed

33
src/detached_inv.lua Normal file
View File

@ -0,0 +1,33 @@
local set_fs = i3.set_fs
IMPORT("fmt", "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()
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)
local function init_detached(player)
local name = player:get_player_name()
local output_rcp = create_inventory(fmt("i3_output_rcp_%s", name), {}, name)
output_rcp:set_size("main", 1)
local output_usg = create_inventory(fmt("i3_output_usg_%s", name), {}, name)
output_usg:set_size("main", 1)
end
return init_detached

View File

@ -1,6 +1,6 @@
local S = core.get_translator "i3" IMPORT("S")
local group_stereotypes = { i3.group_stereotypes = {
dye = "dye:white", dye = "dye:white",
wool = "wool:white", wool = "wool:white",
wood = "default:wood", wood = "default:wood",
@ -17,7 +17,7 @@ local group_stereotypes = {
mesecon_conductor_craftable = "mesecons:wire_00000000_off", mesecon_conductor_craftable = "mesecons:wire_00000000_off",
} }
local group_names = { i3.group_names = {
dye = S"Any dye", dye = S"Any dye",
coal = S"Any coal", coal = S"Any coal",
sand = S"Any sand", sand = S"Any sand",
@ -58,5 +58,3 @@ local group_names = {
["color_dark_grey,dye"] = S"Any dark grey dye", ["color_dark_grey,dye"] = S"Any dark grey dye",
["color_dark_green,dye"] = S"Any dark green dye", ["color_dark_green,dye"] = S"Any dark green dye",
} }
return group_stereotypes, group_names

1444
src/gui.lua Normal file

File diff suppressed because it is too large Load Diff

117
src/hud.lua Normal file
View File

@ -0,0 +1,117 @@
IMPORT("get_player_by_name", "str_to_pos", "add_hud_waypoint")
local function init_hud(player)
local name = player:get_player_name()
local data = i3.data[name]
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 = 4, y = 4},
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,
},
}
core.after(0, function()
player:hud_set_hotbar_itemcount(i3.HOTBAR_LEN)
player:hud_set_hotbar_image"i3_hotbar.png"
end)
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.016
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.HUD_SPEED)
})
end
elseif data.show_hud == false then
if data.hud_timer >= i3.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.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()
for name, data in pairs(i3.data) do
if data.show_hud ~= nil then
local player = get_player_by_name(name)
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

82
src/operators.lua Normal file
View File

@ -0,0 +1,82 @@
--[[ 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 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)
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 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

155
src/progressive.lua Normal file
View File

@ -0,0 +1,155 @@
local set_fs = i3.set_fs
local hud_notif = i3.hud_notif
local POLL_FREQ = 0.25
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, c = {}, 0, 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
c++
items[c] = item
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 groups = 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()
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 "")
hud_notif(data.player_name, msg, "i3_book.png")
end
data.items_raw = 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)

View File

@ -1,8 +1,7 @@
local fmt = string.format
local PNG = { local PNG = {
bg = "i3_bg.png", bg = "i3_bg.png",
bg_full = "i3_bg_full.png", bg_full = "i3_bg_full.png",
bg_content = "i3_bg_content.png",
bar = "i3_bar.png", bar = "i3_bar.png",
hotbar = "i3_hotbar.png", hotbar = "i3_hotbar.png",
search = "i3_search.png", search = "i3_search.png",
@ -12,9 +11,10 @@ local PNG = {
prev = "i3_next.png^\\[transformFX", prev = "i3_next.png^\\[transformFX",
next = "i3_next.png", next = "i3_next.png",
arrow = "i3_arrow.png", arrow = "i3_arrow.png",
arrow_content = "i3_arrow_content.png",
trash = "i3_trash.png", trash = "i3_trash.png",
sort_az = "i3_sort_az.png", sort = "i3_sort.png",
sort_za = "i3_sort_za.png", settings = "i3_settings.png",
compress = "i3_compress.png", compress = "i3_compress.png",
fire = "i3_fire.png", fire = "i3_fire.png",
fire_anim = "i3_fire_anim.png", fire_anim = "i3_fire_anim.png",
@ -32,20 +32,22 @@ local PNG = {
awards = "i3_award.png", awards = "i3_award.png",
skins = "i3_skin.png", skins = "i3_skin.png",
waypoints = "i3_waypoint.png", waypoints = "i3_waypoint.png",
teleport = "i3_teleport.png",
add = "i3_add.png", add = "i3_add.png",
refresh = "i3_refresh.png", refresh = "i3_refresh.png",
visible = "i3_visible.png^\\[brighten", visible = "i3_visible.png^\\[brighten",
nonvisible = "i3_non_visible.png", nonvisible = "i3_non_visible.png",
exit = "i3_exit.png", exit = "i3_exit.png",
home = "i3_home.png",
flag = "i3_flag.png",
edit = "i3_edit.png",
cancel_hover = "i3_cancel.png^\\[brighten", cancel_hover = "i3_cancel.png^\\[brighten",
search_hover = "i3_search.png^\\[brighten", search_hover = "i3_search.png^\\[brighten",
export_hover = "i3_export.png^\\[brighten", export_hover = "i3_export.png^\\[brighten",
trash_hover = "i3_trash.png^\\[brighten^\\[colorize:#f00:100", trash_hover = "i3_trash.png^\\[brighten^\\[colorize:#f00:100",
compress_hover = "i3_compress.png^\\[brighten", compress_hover = "i3_compress.png^\\[brighten",
sort_az_hover = "i3_sort_az.png^\\[brighten", sort_hover = "i3_sort.png^\\[brighten",
sort_za_hover = "i3_sort_za.png^\\[brighten", settings_hover = "i3_settings.png^\\[brighten",
prev_hover = "i3_next_hover.png^\\[transformFX", prev_hover = "i3_next_hover.png^\\[transformFX",
next_hover = "i3_next_hover.png", next_hover = "i3_next_hover.png",
tab_hover = "i3_tab_hover.png", tab_hover = "i3_tab_hover.png",
@ -56,19 +58,21 @@ local PNG = {
awards_hover = "i3_award_hover.png", awards_hover = "i3_award_hover.png",
skins_hover = "i3_skin_hover.png", skins_hover = "i3_skin_hover.png",
waypoints_hover = "i3_waypoint_hover.png", waypoints_hover = "i3_waypoint_hover.png",
teleport_hover = "i3_teleport.png^\\[brighten",
add_hover = "i3_add.png^\\[brighten", add_hover = "i3_add.png^\\[brighten",
refresh_hover = "i3_refresh.png^\\[brighten", refresh_hover = "i3_refresh.png^\\[brighten",
exit_hover = "i3_exit.png^\\[brighten", exit_hover = "i3_exit.png^\\[brighten",
home_hover = "i3_home.png^\\[brighten",
edit_hover = "i3_edit.png^\\[brighten",
} }
local styles = fmt([[ local styles = string.format([[
style_type[field;border=false;bgcolor=transparent] style_type[field;border=false;bgcolor=transparent]
style_type[label,field;font_size=16] style_type[label,field;font_size=16]
style_type[button;border=false;content_offset=0] style_type[button;border=false;content_offset=0]
style_type[image_button,item_image_button;border=false;sound=i3_click] style_type[image_button,item_image_button,checkbox,dropdown;border=false;sound=i3_click]
style_type[item_image_button;bgimg_hovered=%s] style_type[item_image_button;bgimg_hovered=%s]
style[nofav;sound=i3_cannot]
style[pagenum,no_item,no_rcp;font=bold;font_size=18] style[pagenum,no_item,no_rcp;font=bold;font_size=18]
style[exit;fgimg=%s;fgimg_hovered=%s;content_offset=0] style[exit;fgimg=%s;fgimg_hovered=%s;content_offset=0]
style[cancel;fgimg=%s;fgimg_hovered=%s;content_offset=0] style[cancel;fgimg=%s;fgimg_hovered=%s;content_offset=0]
@ -80,10 +84,15 @@ local styles = fmt([[
style[prev_usage;fgimg=%s;fgimg_hovered=%s] style[prev_usage;fgimg=%s;fgimg_hovered=%s]
style[next_usage;fgimg=%s;fgimg_hovered=%s] style[next_usage;fgimg=%s;fgimg_hovered=%s]
style[waypoint_add;fgimg=%s;fgimg_hovered=%s;content_offset=0] 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[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; style[craft_rcp,craft_usg;noclip=true;font_size=16;sound=i3_craft;
bgimg=i3_btn9.png;bgimg_hovered=i3_btn9_hovered.png; bgimg=i3_btn9.png;bgimg_hovered=i3_btn9_hovered.png;
bgimg_pressed=i3_btn9_pressed.png;bgimg_middle=4,6] 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.slot,
PNG.exit, PNG.exit_hover, PNG.exit, PNG.exit_hover,
@ -95,7 +104,8 @@ PNG.prev, PNG.prev_hover,
PNG.next, PNG.next_hover, PNG.next, PNG.next_hover,
PNG.prev, PNG.prev_hover, PNG.prev, PNG.prev_hover,
PNG.next, PNG.next_hover, PNG.next, PNG.next_hover,
PNG.add, PNG.add_hover) PNG.add, PNG.add_hover,
PNG.edit, PNG.edit_hover)
local fs_elements = { local fs_elements = {
label = "label[%f,%f;%s]", label = "label[%f,%f;%s]",
@ -103,6 +113,7 @@ local fs_elements = {
image = "image[%f,%f;%f,%f;%s]", image = "image[%f,%f;%f,%f;%s]",
tooltip = "tooltip[%f,%f;%f,%f;%s]", tooltip = "tooltip[%f,%f;%f,%f;%s]",
button = "button[%f,%f;%f,%f;%s;%s]", button = "button[%f,%f;%f,%f;%s;%s]",
checkbox = "checkbox[%f,%f;%s;%s;%s]",
item_image = "item_image[%f,%f;%f,%f;%s]", item_image = "item_image[%f,%f;%f,%f;%s]",
hypertext = "hypertext[%f,%f;%f,%f;%s;%s]", hypertext = "hypertext[%f,%f;%f,%f;%s;%s]",
bg9 = "background9[%f,%f;%f,%f;%s;false;%u]", bg9 = "background9[%f,%f;%f,%f;%s;false;%u]",
@ -113,4 +124,10 @@ local fs_elements = {
item_image_button = "item_image_button[%f,%f;%f,%f;%s;%s;%s]", item_image_button = "item_image_button[%f,%f;%f,%f;%s;%s;%s]",
} }
return PNG, styles, fs_elements local colors = {
yellow = "#ffd866",
black = "#2d2a2e",
blue = "#7bf",
}
return PNG, styles, fs_elements, colors

View File

@ -0,0 +1,4 @@
i3.compress("default:diamondblock", {
replace = "diamond",
by = {"bronze", "copper", "gold", "steel", "tin"}
})

View File

@ -6,6 +6,11 @@ local mt2 = ItemStack("dye:red")
mt2:get_meta():set_string("description", "test red") mt2:get_meta():set_string("description", "test red")
mt2:get_meta():set_string("color", "#ff0") 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({ minetest.register_craft({
output = mt:to_string(), output = mt:to_string(),
type = "shapeless", type = "shapeless",
@ -15,11 +20,157 @@ minetest.register_craft({
}, },
}) })
minetest.register_craft({
output = mt3:to_string(),
type = "shapeless",
recipe = {
"default:pick_mese",
"default:diamond",
},
})
i3.register_craft {
url = "https://raw.githubusercontent.com/minetest-mods/i3/main/tests/test_online_recipe.json"
}
i3.register_craft({ i3.register_craft({
result = "default:ladder_wood 2", result = "default:ladder_wood 2",
items = {"default:copper_ingot 7, default:tin_ingot, default:steel_ingot 2"}, items = {"default:copper_ingot 7, default:tin_ingot, default:steel_ingot 2"},
}) })
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({ i3.register_craft({
grid = { grid = {
"X #", "X #",

26
tests/test_operators.lua Normal file
View File

@ -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)

View File

@ -1,25 +1,22 @@
i3.new_tab { i3.new_tab("test1", {
name = "test1",
description = "Test 1 Test 1", description = "Test 1 Test 1",
image = "i3_heart.png", image = "i3_heart.png",
formspec = function(player, data, fs) formspec = function(player, data, fs)
fs("label[3,1;Test 1]") fs("label[3,1;Test 1]")
end, end,
} })
i3.new_tab { i3.new_tab("test2", {
name = "test2",
description = "Test 2", description = "Test 2",
image = "i3_mesepick.png", image = "i3_mesepick.png",
formspec = function(player, data, fs) formspec = function(player, data, fs)
fs("label[3,1;Test 2]") fs("label[3,1;Test 2]")
end, end,
} })
i3.new_tab { i3.new_tab("test3", {
name = "test3",
description = "Test 3", description = "Test 3",
access = function(player, data) access = function(player, data)
@ -36,10 +33,9 @@ i3.new_tab {
fields = function(player, data, fields) fields = function(player, data, fields)
i3.set_fs(player, "label[3,2;Test extra_fs]") i3.set_fs(player, "label[3,2;Test extra_fs]")
end, end,
} })
i3.override_tab("test2", { i3.override_tab("test2", {
name = "test2",
description = "Test override", description = "Test override",
image = "i3_mesepick.png", image = "i3_mesepick.png",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
textures/i3_bg_content.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
textures/i3_blank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 B

BIN
textures/i3_edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 528 B

BIN
textures/i3_flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

BIN
textures/i3_home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 570 B

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
textures/i3_settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
textures/i3_sort.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB