Charakterbewegungen hinzugefügt, Deko hinzugefügt, Kochrezepte angepasst

This commit is contained in:
N-Nachtigal 2025-05-14 16:36:42 +02:00
parent 95945c0306
commit a0c893ca0b
1124 changed files with 64294 additions and 763 deletions

11
mods/futil/.cdb.json Normal file
View file

@ -0,0 +1,11 @@
{
"type": "MOD",
"name": "futil",
"title": "futil",
"short_description": "flux's utility mod",
"repo": "https://github.com/fluxionary/minetest-futil.git",
"website": "https://github.com/fluxionary/minetest-futil",
"issue_tracker": "https://github.com/fluxionary/minetest-futil/issues",
"license": "LGPL-3.0-or-later",
"media_license": "CC-BY-SA-4.0"
}

View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
grep $(date -u -I) mod.conf
exit $?

14
mods/futil/.editorconfig Normal file
View file

@ -0,0 +1,14 @@
# See https://editorconfig.org/
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.{lua,luacheckrc}]
indent_style = tab

1
mods/futil/.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
github: fluxionary

View file

@ -0,0 +1,34 @@
name: pre-commit
on: [push, pull_request, workflow_dispatch]
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: update
run: sudo apt-get update -y
- uses: actions/checkout@master
- uses: actions/setup-python@master
- name: install luarocks
run: sudo apt-get install -y luarocks
- name: add luarocks path
run: echo "$HOME/.luarocks/bin" >> $GITHUB_PATH
- name: luacheck install
run: luarocks install --local luacheck
- name: install cargo
run: sudo apt-get install -y cargo
- name: install stylua
run: cargo install stylua
- name: Install pre-commit
run: pip3 install pre-commit
- name: Run pre-commit
run: pre-commit run --all-files

1
mods/futil/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.unfinished.*

658
mods/futil/.luacheckrc Normal file
View file

@ -0,0 +1,658 @@
std = "lua51+luajit+minetest+futil"
unused_args = false
max_line_length = 120
stds.minetest = {
read_globals = {
"DIR_DELIM",
"dump",
"dump2",
"INIT",
math = {
fields = {
abs = {},
acos = {},
asin = {},
atan = {},
atan2 = {},
ceil = {},
cos = {},
cosh = {},
deg = {},
exp = {},
factorial = {},
floor = {},
fmod = {},
frexp = {},
huge = {},
hypot = {},
ldexp = {},
log = {},
log10 = {},
max = {},
min = {},
modf = {},
pi = {},
pow = {},
rad = {},
random = {},
randomseed = {},
round = {},
sign = {},
sin = {},
sinh = {},
sqrt = {},
tan = {},
tanh = {},
},
},
table = {
fields = {
copy = {},
concat = {},
foreach = {},
foreachi = {},
getn = {},
indexof = {},
insert = {},
insert_all = {},
key_value_swap = {},
maxn = {},
move = {},
remove = {},
shuffle = {},
sort = {},
},
},
string = {
fields = {
byte = {},
char = {},
dump = {},
find = {},
format = {},
gmatch = {},
len = {},
lower = {},
match = {},
rep = {},
reverse = {},
split = {},
sub = {},
trim = {},
upper = {},
},
},
vector = {
fields = {
add = {},
angle = {},
apply = {},
check = {},
combine = {},
copy = {},
cross = {},
dir_to_rotation = {},
direction = {},
distance = {},
divide = {},
dot = {},
equals = {},
floor = {},
from_string = {},
in_area = {},
length = {},
metatable = {},
multiply = {},
new = {},
normalize = {},
offset = {},
rotate = {},
rotate_around_axis = {},
round = {},
sort = {},
subtract = {},
to_string = {},
zero = {},
},
},
ItemStack = {
fields = {
add_item = {},
add_wear = {},
add_wear_by_uses = {},
clear = {},
get_count = {},
get_definition = {},
get_description = {},
get_free_space = {},
get_meta = {},
get_metadata = {},
get_name = {},
get_short_description = {},
get_stack_max = {},
get_tool_capabilities = {},
get_wear = {},
is_empty = {},
is_known = {},
item_fits = {},
peek_item = {},
replace = {},
set_count = {},
set_metadata = {},
set_name = {},
set_wear = {},
take_item = {},
to_string = {},
to_table = {},
},
},
PerlinNoise = {
fields = {
get_2d = {},
get_3d = {},
},
},
PerlinNoiseMap = {
fields = {
calc_2d_map = {},
calc_3d_map = {},
get_2d_map = {},
get_2d_map_flat = {},
get_3d_map = {},
get_3d_map_flat = {},
get_map_slice = {},
},
},
PseudoRandom = {
fields = {
next = {},
},
},
PcgRandom = {
fields = {
next = {},
rand_normal_dist = {},
},
},
"Raycast",
SecureRandom = {
fields = {
next_bytes = {},
},
},
Settings = {
fields = {
get = {},
get_bool = {},
get_flags = {},
get_names = {},
get_np_group = {},
remove = {},
set = {},
set_bool = {},
set_np_group = {},
to_table = {},
write = {},
},
},
VoxelArea = {
fields = {
MaxEdge = {},
MinEdge = {},
contains = {},
containsi = {},
containsp = {},
getExtent = {},
getVolume = {},
index = {},
indexp = {},
iter = {},
iterp = {},
new = {},
position = {},
ystride = {},
zstride = {},
},
},
VoxelManip = {
fields = {
calc_lighting = {},
get_data = {},
get_emerged_area = {},
get_light_data = {},
get_node_at = {},
get_param2_data = {},
read_from_map = {},
set_data = {},
set_light_data = {},
set_lighting = {},
set_node_at = {},
set_param2_data = {},
update_liquids = {},
update_map = {},
was_modified = {},
write_to_map = {},
},
},
minetest = {
fields = {
CONTENT_AIR = {},
CONTENT_IGNORE = {},
CONTENT_UNKNOWN = {},
EMERGE_CANCELLED = {},
EMERGE_ERRORED = {},
EMERGE_FROM_DISK = {},
EMERGE_FROM_MEMORY = {},
EMERGE_GENERATED = {},
LIGHT_MAX = {},
MAP_BLOCKSIZE = {},
PLAYER_MAX_BREATH_DEFAULT = {},
PLAYER_MAX_HP_DEFAULT = {},
add_entity = {},
add_item = {},
add_node = {},
add_node_level = {},
add_particle = {},
add_particlespawner = {},
after = {},
async_event_handler = {},
async_jobs = {other_fields = true},
auth_reload = {},
ban_player = {},
builtin_auth_handler = {other_fields = true},
bulk_set_node = {},
calculate_knockback = {},
callback_origins = {other_fields = true},
cancel_shutdown_requests = {},
chat_send_all = {},
chat_send_player = {},
chatcommands = {other_fields = true},
check_for_falling = {},
check_password_entry = {},
check_player_privs = {},
check_single_for_falling = {},
clear_craft = {},
clear_objects = {},
clear_registered_biomes = {},
clear_registered_decorations = {},
clear_registered_ores = {},
clear_registered_schematics = {},
close_formspec = {},
colorize = {},
colorspec_to_bytes = {},
colorspec_to_colorstring = {},
compare_block_status = {},
compress = {},
cpdir = {},
craft_predict = {},
craftitemdef_default = {other_fields = true},
create_detached_inventory = {},
create_detached_inventory_raw = {},
create_schematic = {},
debug = {},
decode_base64 = {},
decompress = {},
delete_area = {},
delete_particlespawner = {},
deserialize = {},
detached_inventories = {other_fields = true},
dig_node = {},
dir_to_facedir = {},
dir_to_wallmounted = {},
dir_to_yaw = {},
disconnect_player = {},
do_async_callback = {},
do_item_eat = {},
dynamic_add_media = {},
dynamic_media_callbacks = {other_fields = true},
emerge_area = {},
encode_base64 = {},
encode_png = {},
env = {other_fields = true},
explode_scrollbar_event = {},
explode_table_event = {},
explode_textlist_event = {},
facedir_to_dir = {},
features = {other_fields = true},
find_node_near = {},
find_nodes_in_area = {},
find_nodes_in_area_under_air = {},
find_nodes_with_meta = {},
find_path = {},
fix_light = {},
forceload_block = {},
forceload_free_block = {},
format_chat_message = {},
formspec_escape = {},
generate_decorations = {},
generate_ores = {},
get_all_craft_recipes = {},
get_artificial_light = {},
get_auth_handler = {},
get_background_escape_sequence = {},
get_ban_description = {},
get_ban_list = {},
get_biome_data = {},
get_biome_id = {},
get_biome_name = {},
get_builtin_path = {},
get_color_escape_sequence = {},
get_connected_players = {},
get_content_id = {},
get_craft_recipe = {},
get_craft_result = {},
get_current_modname = {},
get_day_count = {},
get_decoration_id = {},
get_dig_params = {},
get_dir_list = {},
get_gametime = {},
get_gen_notify = {},
get_heat = {},
get_hit_params = {},
get_humidity = {},
get_inventory = {},
get_item_group = {},
get_last_run_mod = {},
get_mapgen_object = {},
get_mapgen_params = {},
get_mapgen_setting = {},
get_mapgen_setting_noiseparams = {},
get_meta = {},
get_mod_storage = {},
get_modnames = {},
get_modpath = {},
get_name_from_content_id = {},
get_natural_light = {},
get_node = {},
get_node_drops = {},
get_node_group = {},
get_node_level = {},
get_node_light = {},
get_node_max_level = {},
get_node_or_nil = {},
get_node_timer = {},
get_noiseparams = {},
get_objects_in_area = {},
get_objects_inside_radius = {},
get_password_hash = {},
get_perlin = {},
get_perlin_map = {},
get_player_by_name = {},
get_player_information = {},
get_player_ip = {},
get_player_privs = {},
get_player_radius_area = {},
get_pointed_thing_position = {},
get_position_from_hash = {},
get_server_max_lag = {},
get_server_status = {},
get_server_uptime = {},
get_spawn_level = {},
get_timeofday = {},
get_tool_wear_after_use = {},
get_translated_string = {},
get_translator = {},
get_us_time = {},
get_user_path = {},
get_version = {},
get_voxel_manip = {},
get_worldpath = {},
global_exists = {},
handle_async = {},
handle_node_drops = {},
has_feature = {},
hash_node_position = {},
hud_replace_builtin = {},
inventorycube = {},
is_area_protected = {},
is_colored_paramtype = {},
is_creative_enabled = {},
is_nan = {},
is_player = {},
is_protected = {},
is_singleplayer = {},
is_yes = {},
item_drop = {},
item_eat = {},
item_place = {},
item_place_node = {},
item_place_object = {},
item_secondary_use = {},
itemstring_with_color = {},
itemstring_with_palette = {},
kick_player = {},
line_of_sight = {},
load_area = {},
log = {},
luaentities = {other_fields = true},
mkdir = {},
mod_channel_join = {},
mvdir = {},
node_dig = {},
node_punch = {},
nodedef_default = {other_fields = true},
noneitemdef_default = {},
notify_authentication_modified = {},
object_refs = {other_fields = true},
on_craft = {},
override_chatcommand = {},
override_item = {},
parse_coordinates = {},
parse_json = {},
parse_relative_number = {},
place_node = {},
place_schematic = {},
place_schematic_on_vmanip = {},
player_exists = {},
pointed_thing_to_face_pos = {},
pos_to_string = {},
print = {},
privs_to_string = {},
punch_node = {},
raillike_group = {},
raycast = {},
read_schematic = {},
record_protection_violation = {},
register_abm = {},
register_alias = {},
register_alias_force = {},
register_allow_player_inventory_action = {},
register_async_dofile = {},
register_authentication_handler = {},
register_biome = {},
register_can_bypass_userlimit = {},
register_chatcommand = {},
register_craft = {},
register_craft_predict = {},
register_craftitem = {},
register_decoration = {},
register_entity = {},
register_globalstep = {},
register_item = {},
register_lbm = {},
register_node = {},
register_on_auth_fail = {},
register_on_authplayer = {},
register_on_chat_message = {},
register_on_chatcommand = {},
register_on_cheat = {},
register_on_craft = {},
register_on_dieplayer = {},
register_on_dignode = {},
register_on_generated = {},
register_on_item_eat = {},
register_on_joinplayer = {},
register_on_leaveplayer = {},
register_on_liquid_transformed = {},
register_on_mapgen_init = {},
register_on_modchannel_message = {},
register_on_mods_loaded = {},
register_on_newplayer = {},
register_on_placenode = {},
register_on_player_hpchange = {},
register_on_player_inventory_action = {},
register_on_player_receive_fields = {},
register_on_prejoinplayer = {},
register_on_priv_grant = {},
register_on_priv_revoke = {},
register_on_protection_violation = {},
register_on_punchnode = {},
register_on_punchplayer = {},
register_on_respawnplayer = {},
register_on_rightclickplayer = {},
register_on_shutdown = {},
register_ore = {},
register_playerevent = {},
register_privilege = {},
register_schematic = {},
register_tool = {},
registered_abms = {other_fields = true},
registered_aliases = {other_fields = true},
registered_allow_player_inventory_actions = {other_fields = true},
registered_biomes = {other_fields = true},
registered_can_bypass_userlimit = {other_fields = true},
registered_chatcommands = {other_fields = true},
registered_craft_predicts = {other_fields = true},
registered_craftitems = {other_fields = true},
registered_decorations = {other_fields = true},
registered_entities = {other_fields = true},
registered_globalsteps = {other_fields = true},
registered_items = {other_fields = true},
registered_lbms = {other_fields = true},
registered_nodes = {other_fields = true},
registered_on_authplayers = {other_fields = true},
registered_on_chat_messages = {other_fields = true},
registered_on_chatcommands = {other_fields = true},
registered_on_cheats = {other_fields = true},
registered_on_crafts = {other_fields = true},
registered_on_dieplayers = {other_fields = true},
registered_on_dignodes = {other_fields = true},
registered_on_generateds = {other_fields = true},
registered_on_item_eats = {other_fields = true},
registered_on_joinplayers = {other_fields = true},
registered_on_leaveplayers = {other_fields = true},
registered_on_liquid_transformed = {other_fields = true},
registered_on_modchannel_message = {other_fields = true},
registered_on_mods_loaded = {other_fields = true},
registered_on_newplayers = {other_fields = true},
registered_on_placenodes = {other_fields = true},
registered_on_player_hpchange = {other_fields = true},
registered_on_player_hpchanges = {other_fields = true},
registered_on_player_inventory_actions = {other_fields = true},
registered_on_player_receive_fields = {other_fields = true},
registered_on_prejoinplayers = {other_fields = true},
registered_on_priv_grant = {other_fields = true},
registered_on_priv_revoke = {other_fields = true},
registered_on_protection_violation = {other_fields = true},
registered_on_punchnodes = {other_fields = true},
registered_on_punchplayers = {other_fields = true},
registered_on_respawnplayers = {other_fields = true},
registered_on_rightclickplayers = {other_fields = true},
registered_on_shutdown = {other_fields = true},
registered_ores = {other_fields = true},
registered_playerevents = {other_fields = true},
registered_privileges = {other_fields = true},
registered_tools = {other_fields = true},
remove_detached_inventory = {},
remove_detached_inventory_raw = {},
remove_node = {},
remove_player = {},
remove_player_auth = {},
request_http_api = {},
request_insecure_environment = {},
request_shutdown = {},
rgba = {},
rmdir = {},
rollback_get_last_node_actor = {},
rollback_get_node_actions = {},
rollback_punch_callbacks = {other_fields = true},
rollback_revert_actions_by = {},
rotate_and_place = {},
rotate_node = {},
run_callbacks = {},
run_priv_callbacks = {},
safe_file_write = {},
send_join_message = {},
send_leave_message = {},
serialize = {},
serialize_roundtrip = {},
serialize_schematic = {},
set_gen_notify = {},
set_last_run_mod = {},
set_mapgen_params = {},
set_mapgen_setting = {},
set_mapgen_setting_noiseparams = {},
set_node = {},
set_node_level = {},
set_noiseparams = {},
set_player_password = {},
set_player_privs = {},
set_timeofday = {},
setting_get = {},
setting_get_pos = {},
setting_getbool = {},
setting_save = {},
setting_set = {},
setting_setbool = {},
settings = {
fields = {
get = {},
get_bool = {},
get_np_group = {},
get_flags = {},
set = {},
set_bool = {},
set_np_group = {},
remove = {},
get_names = {},
write = {},
to_table = {},
},
},
sha1 = {},
show_formspec = {},
show_general_help_formspec = {},
show_privs_help_formspec = {},
sound_fade = {},
sound_play = {},
sound_stop = {},
spawn_falling_node = {},
spawn_item = {},
spawn_tree = {},
string_to_area = {},
string_to_pos = {},
string_to_privs = {},
strip_background_colors = {},
strip_colors = {},
strip_foreground_colors = {},
strip_param2_color = {},
swap_node = {},
tooldef_default = {other_fields = true},
transforming_liquid_add = {},
translate = {},
unban_player_or_ip = {},
unregister_biome = {},
unregister_chatcommand = {},
unregister_item = {},
wallmounted_to_dir = {},
wrap_text = {},
write_json = {},
yaw_to_dir = {},
},
},
}
}
stds.futil = {
globals = {
"futil",
},
read_globals = {
"fmod",
},
}

View file

@ -0,0 +1,41 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.3.0
hooks:
- id: fix-byte-order-marker
- id: end-of-file-fixer
- id: trailing-whitespace
- id: mixed-line-ending
args: [ --fix=lf ]
- repo: local
hooks:
- id: detect_debug
name: detect debug
language: pygrep
entry: DEBUG
pass_filenames: true
exclude: .pre-commit-config.yaml
fail_fast: true
- id: date_version
name: date version
language: script
entry: .check_date.sh
files: mod.conf
always_run: true
fail_fast: true
- id: stylua
name: stylua
language: system
entry: stylua
pass_filenames: true
types: [ file, lua ]
fail_fast: true
- id: luacheck
name: luacheck
language: system
entry: luacheck
pass_filenames: true
types: [ file, lua ]
args: [ -q ]
fail_fast: true

469
mods/futil/API.md Normal file
View file

@ -0,0 +1,469 @@
# WARNING
this is *VERY OUT OF DATE*. this is a mod for my (flux's) personal use, and maintaining documentation outside the code
isn't worth the time. if other people start using this, i'll reconsider that position.
## classes
* `futil.class1(super)`
a simple class w/ optional inheritance
* `futil.class(...)`
a less simple class w/ multiple inheritance and `is_a` support
## data structures
* `futil.Deque`
a [deque](https://en.wikipedia.org/wiki/Double-ended_queue). supported methods:
* `Deque:size()`
* `Deque:push_front(value)`
* `Deque:push_back(value)`
* `Deque:pop_front()`
* `Deque:pop_back()`
* `futil.PairingHeap`
a [pairing heap](https://en.wikipedia.org/wiki/Pairing_heap). supported methods:
* `PairingHeap:size()`
* `PairingHeap:peek_max()`
* `PairingHeap:delete(value)`
* `PairingHeap:delete_max()`
* `PairingHeap:get_priority(value)`
* `PairingHeap:set_priority(value, priority)`
* `futil.DefaultTable`
a table in which missing keys are automatically filled in. example usage:
```lua
local default_table = futil.DefaultTable(function(key) return {} end)
default_table.foo.bar = 100 -- foo is automatically created as a table
```
## general routines
* `futil.check_call(func)`
wraps `func` in a pcall. if no error occurs, returns the results. otherwise, logs and returns nil.
* `futil.memoize1(f)`
memoize a single-argument function
* `futil.memoize_dumpable(f)`
memoize a function if the arguments produce a unique result when `dump()`-ed
* `futil.memoize1_modstorage(id, func)`
memoize a function and store the results in modstorage, so they persist between sessions.
* `futil.truncate(s, max_length, suffix)`
if the string is longer than max_length, truncate it and append suffix. suffix is optional, defaults to "..."
* `futil.lc_cmp(a, b)`
case-insensitive comparator
* `futil.table.set_all(t1, t2)`
sets all key/value pairs of t2 in t1
* `futil.table.pairs_by_value(t, sort_function)`
iterator which returns key/value pairs, sorted by value
* `futil.table.pairs_by_key(t, sort_function)`
iterator which returns key/value pairs, sorted by key
* `futil.table.size(t)`
gets the size of a table
* `futil.table.is_empty(t)`
returns true if the table is empty
* `futil.equals(a, b)`
returns true if the tables (or other values) are equivalent. do not use w/ recursive structures.
currently does not inspect metatables.
* `futil.table.count_elements(t)`
given a table in which some values may repeat, returns a table mapping values to their count.
* `futil.table.sets_intersect(set1, set2)`
returns true if `set1` and `set2` have any keys in common.
* `futil.table.iterate(t)`
iterates the values of an array-like table
* `futil.table.reversed(t)`
returns a reversed copy of the table.
* `futil.table.contains(t, value)`
returns `true` if value is in table
* `futil.table.keys(t)`
returns a table of the keys in the given tables.
* `futil.table.values(t)`
returns a table of the values in the given tables.
* `futil.table.sort_keys(t, sort_function)`
returns a table of the sorted keys of the given table.
* `futil.wait(n)`
busy-waits n microseconds
* `futil.file_exists(path)`
returns true if the path points to a file that can be opened
* `futil.load_file(filename)`
returns the contents of the file if it exists, otherwise nil.
* `futil.write_file(filename, contents)`
writes to a file. returns true if success, false if not.
* `futil.path_concat(...)`
concatenates part of a file path.
* `futil.path_split(path)`
splits a path into parts.
* `futil.string.truncate(s, max_length, suffix)`
truncate a string if it is longer than max_length, adding suffix (default "...").
* `futil.string.lc_cmp(a, b)`
compares the lower-case values of strings a and b.
* `futil.seconds_to_interval(time)`
transforms a time (in seconds) to a format like "\[\[\[\[<years>:]<days>:]<hours>:]<minutes>:]<seconds>"p
* `futil.format_utc(timestamp)`
formats a timestamp in UTC.
### predicates
* `futil.is_nil(v)`
true if v is `nil`
* `futil.is_boolean(v)`
true if `v` is a boolean.
* `futil.is_number(v)`
true if `v` is a number.
* `futil.is_string(v)`
true if `v` is a string.
* `futil.is_userdata(v)`
true if `v` is userdata.
* `futil.is_function(v)`
true if `v` is a function.
* `futil.is_thread(v)`
true if `v` is a thread.
* `futil.is_table(v)`
true if `v` is a table.
### functional
* `futil.functional.noop()`
the NOTHING function does nothing.
* `futil.functional.identity(x)`
returns x
* `futil.functional.izip(...)`
[zips](https://docs.python.org/3/library/functions.html#zip) iterators.
* `futil.functional.zip(...)`
[zips](https://docs.python.org/3/library/functions.html#zip) tables.
* `futil.functional.imap(func, ...)`
maps a function to a sequence of iterators. the first arg to func is the first element of each iterator, etc.
* `futil.functional.map(func, ...)`
maps a function to a sequence of tables. the first arg to func is the first element of each table, etc.
* `futil.functional.apply(func, t)`
for all keys `k`, set `t[k] = func(t[k])`
* `futil.functional.reduce(func, t, initial)`
applies binary function `func` to successive elements in t and a "total". supply `initial` if possibly `#t == 0`.
e.g. `local sum = function(values) return reduce(function(a, b) return a + b end, values, 0) end`.
* `futil.functional.partial(func, ...)`
curries `func`. `partial(func, a, b, c)(d, e, f) == func(a, b, c, d, e, f)
* `futil.functional.compose(a, b)`
binary operator which composes two functions. `compose(a, b)(x) == a(b(x))`
* `futil.functional.ifilter(pred, i)`
returns an interator which returns the values of iterator `i` which match predicate `pred`
* `futil.functional.filter(pred, t)`
returns an interator which returns the values of table `t` which match predicate `pred`
* `futil.functional.iall(i)`
given an iterator, returns true if all non-nil values of the iterator are not false.
* `futil.functional.all(t)`
given a table, returns true if the table doesn't contain any `false` values
* `futil.functional.iany(i)`
given an iterator, returns true if the iterator produces any non-false values.
* `futil.functional.any(t)`
given a table, returns true if it contains any non-false values.
### iterators
* `futil.iterators.range(...)`
* one arg: return an iterator from 1 to x.
* two args: return an iterator from x to y
* three args: return an iterator from x to y, incrementing by z
* `iterators.repeat_(value, times)`
* times = nil: return `value` forever
* times = positive number: return `value` `times` times
* `futil.iterators.chain(...)`
given a sequence of iterators, return an iterator which will return the values from each in turn.
* `futil.iterators.count(start, step)`
returns an infinite iterator which counts from start by step. if step is not specified, counts by 1.
* `futil.iterators.values(t)`
returns an iterator of the values in the table.
* `futil.list(iterator)`
given an iterator, returns a table of the values of the iterator.
* `futil.list_multiple(iterator)`
given an iterator which returns multiple values on each step, create a table of tables of those values.
### math
* `futil.math.idiv(a, b)`
returns the whole part of a division and the remainder, e.g. `math.floor(a/b), a%b`.
* futil.math.bound(m, v, M)
if v is less than m, return m. if v is greater than M, return M. else return v.
* futil.math.in_bounds(m, v, M)
return true if m <= v and v <= M
* futil.math.is_integer(v)
returns true if v is an integer.
* futil.math.is_u8(i)
returns true if i is a valid unsigned 8 bit value.
* futil.math.is_u16(i)
returns true if i is an unsigned 16 bit value.
* `futil.math.sum(t, initial)`
given a table, get the sum of the values in the table. initial is the value from which to start counting.
if initial is nil and the table is empty, will return nil.
* `futil.math.isum(i, initial)`
like the above, but given an iterator.
## minetest-specific routines
* `futil.add_groups(itemstring, new_groups)`
`new_groups` should be a table of groups to add to the item's existing groups
* `futil.remove_groups(itemstring, ...)`
`...` should be a list of groups to remove from the item's existing groups
* `futil.get_items_with_group(group)`
returns a list of itemstrings which belong to the specified group
* `futil.get_location_string(inv)`
given an `InvRef`, get a location string suitable for use in formspec
* `futil.resolve_item(item)`
given an itemstring or `ItemStack`, follows aliases until it finds the real item.
returns an itemstring.
* `futil.items_equals(item1, item2)`
returns true if two itemstrings/stacks represent identical stacks.
* `futil.get_blockpos(pos)`
converts a position vector into a blockpos
* `futil.get_block_bounds(blockpos)`
gets the bound vectors of a blockpos
* `futil.formspec_pos(pos)`
convert a position into a string suitable for use in formspecs
* `futil.iterate_area(minp, maxp)`
creates an iterator for every point in the volume between minp and maxp
* `futil.iterate_volume(pos, radius)`
like the above, given a position and radius (L∞ metric)
* `futil.serialize(x)`
turns a simple lua data structure (e.g. a table no userdata or functions) into a string
* `futil.deserialize(data)`
the reverse of the above. not safe; do not use w/ untrusted data
* `futil.strip_translation(msg)`
strips minetest's translation escape sequences from a message
* `futil.get_safe_short_description(item)`
gets a short description which won't contain unmatched translation escapes
* `futil.escape_texture(texturestring)`
escapes a texture modifier, for use within another modifier
* `futil.get_horizontal_speed(player)`
get's a player's horizontal speed.
* `futil.is_on_ground(player)`
returns true if a player is standing on the ground.
NOTE: this is currently unfinished, and doesn't report correctly if a player is standing on things with complex
collision boxes which are rotated via `paramtype2="facedir"` or similar.
### fake inventory
this is useful for testing multiple actions on an inventory without having to worry about changing the inventory or
reverting it. this is a better solution than a detached inventory, as actions on a detached inventory are still sent
to clients. fake inventories support all the regular methods of a
[minetest inventory object](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#invref),
with some additions.
* `futil.FakeInventory()`
create a fake inventory.
* `futil.FakeInventory.create_copy(inv)`
copy all the inventory lists from inv into a new fake inventory. will also create a copy of another fake inventory.
* `futil.FakeInventory.room_for_all(inv, listname, items)`
create a copy of inv, then tests if all items in the list `items` can be inserted into listname.
### globalstep
implements common boilerplate for globalsteps which are intended to execute every so often.
```lua
futil.register_globalstep({
period = 1, -- the globalstep should be run every <period> seconds
catchup = "single", -- whether to "catch up" if lag prevents the callback from running
-- if not specified, no catchup will be attempted.
-- if "single", the callback will be run at most once per server-step until we've caught up.
-- if "full", will re-run the callback within the current step until we've caught up.
func = function(dtime) end, -- code to execute
})
```
### hud manager
code to manage HUDs
```lua
local hud = futil.define_hud("my_hud", {
period = nil, -- if a number is given, will automatically update the hud for all players every <period> seconds.
name_field = nil, -- which hud field to use to store an identifier. this should be a field not used by the given
-- hud_elem_type. defaults to "name", which is good for most types. waypoints are an exception.
get_hud_def = function(player) return {} end, -- return the expected hud definition for the player.
-- if nil is returned, the hud will be removed.
enabled_by_default = false, -- whether the hud should be enabled by default.
})
local player = minetest.get_player_by_name("flux")
if hud:toggle_enabled(player) then
print("hud now enabled")
else
print("hud now disabled")
end
print("hud is " .. (hud:is_enabled(player) and "enabled" or "disabled"))
hud:update(player) -- calls hud.get_hud_def(player) and updates the players hud
```

168
mods/futil/LICENSE.txt Normal file
View file

@ -0,0 +1,168 @@
this license is for the code.
any non-code media included in this repository is covered by the contents of MEDIA_LICENSE.txt.
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View file

@ -0,0 +1,427 @@
Attribution-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-ShareAlike 4.0 International Public
License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-ShareAlike 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
l. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
m. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.

5
mods/futil/README.md Normal file
View file

@ -0,0 +1,5 @@
# futil - flux's utility mod
a bunch of simple lua routines and data structures. this is a library which is used by other mods, which doesn't change
any behavior or add any features itself. it mostly provides the creator (flux) w/ a toolkit for solving common problems
without having to re-implement the same code in multiple mods.

View file

@ -0,0 +1,125 @@
-- pack bits into doubles
-- this is quite slow compared to how you'd do this in c or something.
-- there's https://bitop.luajit.org/api.html but that seems limited to 32 bits. we can use the full 53 bit mantissa.
local BITS_PER_NUMBER = 53
local f = string.format
local m_floor = math.floor
local s_byte = string.byte
local s_char = string.char
local BitArray = futil.class1()
function BitArray:_init(size_or_bitmap)
if type(size_or_bitmap) == "number" then
local data = {}
self._size = size_or_bitmap
for i = 1, math.ceil(size_or_bitmap / BITS_PER_NUMBER) do
data[i] = 0
end
self._data = data
elseif type(size_or_bitmap) == "table" then
if size_or_bitmap.is_a and size_or_bitmap:is_a(BitArray) then
self._size = size_or_bitmap._size
self._data = table.copy(size_or_bitmap._data)
end
end
if not self._data then
error("bitmap must be initialized w/ a size or another bitmap")
end
end
function BitArray:__eq(other)
if self._size ~= other._size then
return false
end
for i = 1, self._size do
if self._data[i] ~= other._data[i] then
return false
end
end
return true
end
local function get_bit(n, j)
n = n % (2 ^ j)
return m_floor(n / (2 ^ (j - 1))) == 1
end
function BitArray:get(k)
if type(k) ~= "number" then
return nil
elseif k <= 0 or k > self._size then
return nil
end
local i = math.ceil(k / BITS_PER_NUMBER)
local n = self._data[i]
local j = ((k - 1) % BITS_PER_NUMBER) + 1
return get_bit(n, j)
end
local function set_bit(n, j, v)
local current = get_bit(n, j)
if current == v then
return n
elseif v then
return n + (2 ^ (j - 1))
else
return n - (2 ^ (j - 1))
end
end
function BitArray:set(k, v)
if type(v) == "number" then
if v < 0 or v > 1 then
error(f("invalid argument %s", v))
end
v = v == 1
elseif type(v) ~= "boolean" then
error(f("invalid argument of type %s", type(v)))
end
local i = math.ceil(k / BITS_PER_NUMBER)
local n = self._data[i]
local j = ((k - 1) % BITS_PER_NUMBER) + 1
self._data[i] = set_bit(n, j, v)
end
function BitArray:serialize()
local data = self._data
local parts = {}
for i = 1, #data do
local datum = data[i]
parts[i] = s_char(datum % 256)
.. s_char(m_floor(datum / 256) % 256)
.. s_char(m_floor(datum / (256 ^ 2)) % 256)
.. s_char(m_floor(datum / (256 ^ 3)) % 256)
.. s_char(m_floor(datum / (256 ^ 4)) % 256)
.. s_char(m_floor(datum / (256 ^ 5)) % 256)
.. s_char(m_floor(datum / (256 ^ 6)) % 256)
end
return table.concat(parts, "")
end
function BitArray.deserialize(s)
if type(s) ~= "string" then
error(f("invalid argument of type %s", type(s)))
elseif #s % 7 ~= 0 then
error(f("invalid serialized string (wrong length)"))
end
local ba = BitArray(#s / 7)
local i = 1
for a = 1, #s, 7 do
local bs = {}
for j = 0, 6 do
bs[j + 1] = s_byte(s:sub(a + j, a + j))
end
ba._data[i] = bs[1]
+ 256 * (bs[2] + 256 * (bs[3] + 256 * (bs[4] + 256 * (bs[5] + 256 * (bs[6] + 256 * bs[7])))))
i = i + 1
end
return ba
end
futil.BitArray = BitArray

View file

@ -0,0 +1,11 @@
-- https://www.lua.org/pil/13.4.3.html
function futil.DefaultTable(initializer)
return setmetatable({}, {
__index = function(t, k)
local v = initializer(k)
t[k] = v
return v
end,
})
end

View file

@ -0,0 +1,88 @@
-- inspired by https://www.lua.org/pil/11.4.html
local Deque = futil.class1()
function Deque:_init(def)
self._a = 0
self._z = -1
self._m = def and def.max_size
end
function Deque:size()
return self._z - self._a + 1
end
function Deque:push_front(value)
local max_size = self._m
if max_size and (self._z - self._a + 1) >= max_size then
return false
end
local a = self._a - 1
self._a = a
self[a] = value
return true, a
end
function Deque:peek_front()
return self[self._a]
end
function Deque:pop_front()
local a = self._a
if a > self._z then
return nil
end
local value = self[a]
self[a] = nil
self._a = a + 1
return value
end
function Deque:push_back(value)
local max_size = self._m
if max_size and (self._z - self._a + 1) >= max_size then
return false
end
local z = self._z + 1
self._z = z
self[z] = value
return true, z
end
function Deque:peek_back()
return self[self._z]
end
function Deque:pop_back()
local z = self._z
if self._a > z then
return nil
end
local value = self[z]
self[z] = nil
self._z = z + 1
return value
end
-- this iterator is kinda wonky, and the behavior may be changed in the future.
-- unexpected behavior may result from modifying a deque *while* iterating it.
-- note that you *cannot* iterate the deque directly using `pairs()` because of e.g. "_a" and "_z"
function Deque:iterate()
local i = self._a - 1
return function()
i = i + 1
return self[i]
end
end
function Deque:clear()
for k in pairs(self) do
if type(k) == "number" then
self[k] = nil
end
end
self._a = 0
self._z = -1
end
futil.Deque = Deque

View file

@ -0,0 +1,7 @@
futil.dofile("data_structures", "bitarray")
futil.dofile("data_structures", "default_table")
futil.dofile("data_structures", "deque")
futil.dofile("data_structures", "pairing_heap")
futil.dofile("data_structures", "point_search_tree")
futil.dofile("data_structures", "set")
futil.dofile("data_structures", "sparse_graph") -- requires default_table and set

View file

@ -0,0 +1,156 @@
-- https://en.wikipedia.org/wiki/Pairing_heap
-- https://www.cs.cmu.edu/~sleator/papers/pairing-heaps.pdf
-- https://www.cise.ufl.edu/~sahni/dsaaj/enrich/c13/pairing.htm
local inf = math.huge
local function add_child(node1, node2)
node2.parent = node1
node2.sibling = node1.child
node1.child = node2
end
local function meld(node1, node2)
if node1 == nil or node1.value == nil then
return node2
elseif node2 == nil or node2.value == nil then
return node1
elseif node1.priority > node2.priority then
add_child(node1, node2)
return node1
else
add_child(node2, node1)
return node2
end
end
local function merge_pairs(node)
if node.value == nil or not node.sibling then
return
end
local sibling = node.sibling
local siblingsibling = sibling.sibling
node.sibling = nil
sibling.sibling = nil
node = meld(node, sibling)
if siblingsibling then
return meld(node, merge_pairs(siblingsibling))
else
return node
end
end
local function cut(node)
local parent = node.parent
if parent.child == node then
parent.child = node.sibling
else
parent = parent.child
local sibling = parent.sibling
while sibling ~= node do
parent = sibling
sibling = parent.sibling
end
parent.sibling = node.sibling
end
node.parent = nil
node.sibling = nil
end
local function need_to_move(node, new_priority)
local cur_priority = node.priority
if cur_priority < new_priority then
-- priority increase, make sure we don't dominate our parent
local parent = node.parent
return (parent and new_priority > parent.priority)
elseif cur_priority > new_priority then
-- priority decrease, make sure our children don't dominate us
local child = node.child
while child and child ~= node do
if child.priority > new_priority then
return true
end
child = child.sibling
end
return false
else
return false
end
end
local PairingHeap = futil.class1()
function PairingHeap:_new()
self._nodes_by_value = {}
self._size = 0
end
function PairingHeap:size()
return self._size
end
function PairingHeap:peek()
local hn = self._max_node
if not hn then
error("empty")
end
return hn.value, hn.priority
end
function PairingHeap:remove(value)
self:set_priority(value, inf)
return self:pop()
end
function PairingHeap:pop()
local max = self._max_node
if not max then
error("empty")
end
local child = max.child
if child then
self._max_node = merge_pairs(child)
end
local value = max._value
self._nodes_by_value[value] = nil
self._size = self._size - 1
return value
end
function PairingHeap:get_priority(value)
return self._nodes_by_value[value].priority
end
function PairingHeap:set_priority(value, priority)
local cur_node = self._nodes_by_value[value]
if cur_node then
local need_to = need_to_move(cur_node, priority)
if need_to then
cut(cur_node)
self._max_node = meld(cur_node, self._max_node)
else
cur_node.priority = priority
end
else
local node = { value = value, priority = priority }
self._nodes_by_value[value] = node
self._max_node = meld(self._max_node, node)
self._size = self._size + 1
end
end
futil.PairingHeap = PairingHeap

View file

@ -0,0 +1,268 @@
--[[
a data structure which can efficiently retrieve values within specific rectangular regions of 3d space.
the closest relevant descriptions of this problem and solution are in the following:
https://en.wikipedia.org/wiki/Min/max_kd-tree
https://medium.com/omarelgabrys-blog/geometric-applications-of-bsts-e58f0a5019f3
creation is O(n log n)
finding objects in a region is O(m + log n) if there's m objects in the region.
the hope here is that this will provide a faster alternative to `minetest.get_objects_in_area()`. that currently
iterates over *all* active objects in the world, which can be slow when there's ten thousand or more of objects in the
world, and you are only interested in a few of them. in particular, the your-land server usually has 5-8 thousand
objects loaded at once, and can have hundreds of mobs calling `get_objects_in_area` every couple server steps. perftop
definitively implicated this routine as being a major source of lag. the question, now, is what to do about it.
the current implementation doesn't allow for insertion, deletion, or changes in location. if your points move around,
you're gonna have to rebuild the whole tree from scratch, which currently limits the usefulness.
TODO: read this and incorporate if applicable: https://arxiv.org/abs/1410.5420
== footnotes about the algorithms ==
currently, we're hard-coding the usage of the [median of medians](https://en.wikipedia.org/wiki/Median_of_medians)
algorithm as the pivot strategy, as this resulted in unexpectedly dramatic improvements over random selection in
some informal performance tests i did.
== footnotes about results ==
currently, performance can range from taking 1/20th of the time of the engine call, to 100 times as long. this
makes me realize that this is worth pursuing, but probably this will need to be ported to c++ to consistently provide
a benefit. but, i'll absolutely need to figure out a self-balancing strategy before that'll be appropriate.
]]
local sort = table.sort
local in_area = vector.in_area
or function(pos, pmin, pmax)
local x, y, z = pos.x, pos.y, pos.z
return pmin.x <= x and x <= pmax.x and pmin.y <= y and y <= pmax.y and pmin.z <= z and z <= pmax.z
end
local axes = { "x", "y", "z" }
local POS = 1
local VALUE = 2
local Leaf = futil.class1()
function Leaf:_init(pos_and_value)
self[POS] = pos_and_value[POS]
self[VALUE] = pos_and_value[VALUE]
end
local Node = futil.class1()
function Node:_init(min, max, left, right)
self.min = min
self.max = max
self.left = left
self.right = right
end
local PointSearchTree = futil.class1()
futil.min_median_max = {}
function futil.min_median_max.sort(t, indexer)
if indexer then
sort(t, function(a, b)
return indexer(a) < indexer(b)
end)
else
sort(t)
end
return t[1], math.floor(#t / 2), t[#t]
end
function futil.min_median_max.gen_select(pivot_alg)
return function(t, indexer)
local median = futil.selection.select(t, pivot_alg, function(a, b)
return indexer(a) < indexer(b)
end)
local min = indexer(t[1])
local max = min
for i = 2, #t do
local v = indexer(t[i])
if v < min then
min = v
elseif v > max then
max = v
end
end
return min, median, max
end
end
local function bisect(pos_and_values, axis_i, min_median_max)
if #pos_and_values == 1 then
return Leaf(pos_and_values[1])
end
local axis = axes[axis_i]
local min, median, max = min_median_max(pos_and_values, function(i)
return i[POS][axis]
end)
local next_axis_i = (axis_i % #axes) + 1
return Node(
min,
max,
bisect({ unpack(pos_and_values, 1, median) }, next_axis_i, min_median_max),
bisect({ unpack(pos_and_values, median + 1) }, next_axis_i, min_median_max)
)
end
function PointSearchTree:_init(pos_and_values, min_median_max)
pos_and_values = pos_and_values or {}
min_median_max = min_median_max or futil.min_median_max.gen_select(futil.selection.pivot.median_of_medians)
self._len = #pos_and_values
if #pos_and_values > 0 then
self._root = bisect(pos_and_values, 1, min_median_max)
end
end
-- -DLUAJIT_ENABLE_LUA52COMPAT
function PointSearchTree:__len()
return self._len
end
function PointSearchTree:dump()
local function getlines(node, axis_i)
local axis = axes[axis_i]
if not node then
return {}
elseif node:is_a(Leaf) then
return { minetest.pos_to_string(node[POS], 1) }
else
local lines = {}
for _, line in ipairs(getlines(node.left, (axis_i % #axes) + 1)) do
lines[#lines + 1] = string.format("%s=[%.1f,%.1f] %s", axis, node.min, node.max, line)
end
for _, line in ipairs(getlines(node.right, (axis_i % #axes) + 1)) do
lines[#lines + 1] = string.format("%s=[%.1f,%.1f] %s", axis, node.min, node.max, line)
end
return lines
end
end
return table.concat(getlines(self._root, 1), "\n")
end
local function make_iterator(pmin, pmax, predicate, accumulate)
local function iterate(node, axis_i)
local next_axis_i = (axis_i % 3) + 1
local next_axis = axes[next_axis_i]
local left = node.left
if left then
if left:is_a(Leaf) then
if predicate(left) then
accumulate(left)
end
elseif pmin[next_axis] <= left.max then
iterate(left, next_axis_i)
end
end
local right = node.right
if right then
if right:is_a(Leaf) then
if predicate(right) then
accumulate(right)
end
elseif right.min <= pmax[next_axis] then
iterate(right, next_axis_i)
end
end
end
return iterate
end
function PointSearchTree:iterate_values_in_area(pmin, pmax)
if not self._root then
return function() end
end
pmin, pmax = vector.sort(pmin, pmax)
if self._root.max < pmin.x or pmax.x < self._root.min then
return function() end
end
return coroutine.wrap(function()
make_iterator(pmin, pmax, function(leaf)
return in_area(leaf[POS], pmin, pmax)
end, function(leaf)
coroutine.yield(leaf[POS], leaf[VALUE])
end)(self._root, 1)
end)
end
function PointSearchTree:get_values_in_area(pmin, pmax)
local via = {}
if not self._root then
return via
end
pmin, pmax = vector.sort(pmin, pmax)
if self._root.max < pmin.x or pmax.x < self._root.min then
return via
end
make_iterator(pmin, pmax, function(leaf)
return in_area(leaf[POS], pmin, pmax)
end, function(leaf)
via[#via + 1] = leaf[VALUE]
end)(self._root, 1)
return via
end
function PointSearchTree:iterate_values_inside_radius(center, radius)
if not self._root then
return function() end
end
local pmin = vector.subtract(center, radius)
local pmax = vector.add(center, radius)
if self._root.max < pmin.x or pmax.x < self._root.min then
return function() end
end
local v_distance = vector.distance
return coroutine.wrap(function()
make_iterator(pmin, pmax, function(leaf)
return v_distance(center, leaf[POS]) <= radius
end, function(leaf)
coroutine.yield(leaf[POS], leaf[VALUE])
end)(self._root, 1)
end)
end
function PointSearchTree:get_values_inside_radius(center, radius)
local vir = {}
if not self._root then
return vir
end
local pmin = vector.subtract(center, radius)
local pmax = vector.add(center, radius)
if self._root.max < pmin.x or pmax.x < self._root.min then
return vir
end
local v_distance = vector.distance
make_iterator(pmin, pmax, function(leaf)
return v_distance(center, leaf[POS]) <= radius
end, function(leaf)
vir[#vir + 1] = leaf[VALUE]
end)(self._root, 1)
return vir
end
futil.PointSearchTree = PointSearchTree

View file

@ -0,0 +1,267 @@
-- based more-or-less on python's set
local f = string.format
local Set = futil.class1()
local function is_a_set(thing)
return type(thing) == "table" and type(thing.is_a) == "function" and thing:is_a(Set)
end
function Set:_init(t_or_i)
self._size = 0
self._set = {}
if t_or_i then
if type(t_or_i.is_a) == "function" then
if t_or_i:is_a(Set) then
self._set = table.copy(t_or_i._set)
self._size = t_or_i._size
else
for v in t_or_i:iterate() do
self:add(v)
end
end
elseif type(t_or_i) == "table" then
for i = 1, #t_or_i do
self:add(t_or_i[i])
end
elseif type(t_or_i) == "function" or getmetatable(t_or_i).__call then
for v in t_or_i do
self:add(v)
end
else
error(f("unknown argument of type %s", type(t_or_i)))
end
end
end
-- turn a table like {foo=true, bar=true} into a Set
function Set.convert(t)
local set = Set()
set._set = t
set._size = futil.table.size(t)
return set
end
-- -DLUAJIT_ENABLE_LUA52COMPAT
function Set:__len()
return self._size
end
function Set:len()
return self._size
end
function Set:size()
return self._size
end
function Set:is_empty()
return self._size == 0
end
function Set:__tostring()
local elements = {}
for element in pairs(self._set) do
elements[#elements + 1] = f("%q", element)
end
return f("Set({%s})", table.concat(elements, ", "))
end
function Set:__eq(other)
if not is_a_set(other) then
return false
end
for k in pairs(self._set) do
if not other._set[k] then
return false
end
end
return self._size == other._size
end
function Set:contains(element)
return self._set[element] == true
end
function Set:add(element)
if not self:contains(element) then
self._set[element] = true
self._size = self._size + 1
end
end
function Set:remove(element)
if not self:contains(element) then
error(f("set does not contain %s", element))
end
self._set[element] = nil
self._size = self._size - 1
end
function Set:discard(element)
if self:contains(element) then
self._set[element] = nil
self._size = self._size - 1
end
end
function Set:clear()
self._set = {}
self._size = 0
end
function Set:iterate()
return futil.table.ikeys(self._set)
end
function Set:intersects(other)
if not is_a_set(other) then
other = Set(other)
end
local smaller, bigger
if other:size() < self:size() then
smaller = other
bigger = self
else
smaller = self
bigger = other
end
for element in smaller:iterate() do
if bigger:contains(element) then
return true
end
end
return false
end
function Set:isdisjoint(other)
if not is_a_set(other) then
other = Set(other)
end
local smaller, bigger
if other:size() < self:size() then
smaller = other
bigger = self
else
smaller = self
bigger = other
end
for element in smaller:iterate() do
if bigger:contains(element) then
return false
end
end
return true
end
function Set:issubset(other)
if not is_a_set(other) then
other = Set(other)
end
if self:size() > other:size() then
return false
end
for element in self:iterate() do
if not other:contains(element) then
return false
end
end
return true
end
function Set:__le(other)
return self:issubset(other)
end
function Set:__lt(other)
if not is_a_set(other) then
other = Set(other)
end
if self:size() >= other:size() then
return false
end
for element in self:iterate() do
if not other:contains(element) then
return false
end
end
return true
end
function Set:issuperset(other)
if not is_a_set(other) then
other = Set(other)
end
return other:issubset(self)
end
function Set:update(other)
if not is_a_set(other) then
other = Set(other)
end
for element in other:iterate() do
self:add(element)
end
end
function Set:union(other)
if not is_a_set(other) then
other = Set(other)
end
local union = Set(self)
union:update(other)
return union
end
function Set:__add(other)
return self:union(other)
end
function Set:intersection_update(other)
if not is_a_set(other) then
other = Set(other)
end
for element in self:iterate() do
if not other:contains(element) then
self:remove(element)
end
end
end
function Set:intersection(other)
if not is_a_set(other) then
other = Set(other)
end
local intersection = Set()
for element in self:iterate() do
if other:contains(element) then
intersection:add(element)
end
end
return intersection
end
function Set:difference_update(other)
if not is_a_set(other) then
other = Set(other)
end
for element in other:iterate() do
self:discard(element)
end
end
function Set:difference(other)
if not is_a_set(other) then
other = Set(other)
end
local difference = Set(self)
difference:difference_update(other)
return difference
end
function Set:__sub(other)
return self:difference(other)
end
futil.Set = Set

View file

@ -0,0 +1,32 @@
local f = string.format
local SparseGraph = futil.class1()
function SparseGraph:_init(size)
self._size = size or 0
self._adj_by_vertex = futil.DefaultTable(function()
return futil.Set()
end)
end
function SparseGraph:size()
return self._size
end
function SparseGraph:add_vertex()
self._size = self._size + 1
end
function SparseGraph:add_edge(a, b)
assert(1 <= a and a <= self._size, f("invalid vertex a %s", a))
assert(1 <= b and b <= self._size, f("invalid vertex b %s", b))
self._adj_by_vertex[a]:add(b)
end
function SparseGraph:has_edge(a, b)
assert(1 <= a and a <= self._size, f("invalid vertex a %s", a))
assert(1 <= b and b <= self._size, f("invalid vertex b %s", b))
return self._adj_by_vertex[a]:contains(b)
end
futil.SparseGraph = SparseGraph

11
mods/futil/init.lua Normal file
View file

@ -0,0 +1,11 @@
fmod.check_version({ year = 2023, month = 7, day = 14 }) -- async dofile
futil = fmod.create()
futil.dofile("util", "init")
futil.dofile("data_structures", "init") -- depends on util
futil.dofile("minetest", "init") -- depends on util and data_structures
if INIT == "game" then
futil.async_dofile("init")
end

186
mods/futil/minetest/box.lua Normal file
View file

@ -0,0 +1,186 @@
-- box definition below node boxes: https://github.com/minetest/minetest/blob/master/doc/lua_api.md#node-boxes
local x1 = 1
local y1 = 2
local z1 = 3
local x2 = 4
local y2 = 5
local z2 = 6
function futil.boxes_intersect(box1, box2)
return not (
(box1[x2] < box2[x1] or box2[x2] < box1[x1])
or (box1[y2] < box2[y1] or box2[y2] < box1[y1])
or (box1[z2] < box2[z1] or box2[z2] < box1[z1])
)
end
function futil.box_offset(box, number_or_vector)
if type(number_or_vector) == "number" then
return {
box[1] + number_or_vector,
box[2] + number_or_vector,
box[3] + number_or_vector,
box[4] + number_or_vector,
box[5] + number_or_vector,
box[6] + number_or_vector,
}
else
return {
box[1] + number_or_vector.x,
box[2] + number_or_vector.y,
box[3] + number_or_vector.z,
box[4] + number_or_vector.x,
box[5] + number_or_vector.y,
box[6] + number_or_vector.z,
}
end
end
function futil.is_box(box)
if type(box) == "table" and #box == 6 then
for _, x in ipairs(box) do
if type(x) ~= "number" then
return false
end
end
return box[1] <= box[4] and box[2] <= box[5] and box[3] <= box[6]
end
return false
end
function futil.is_boxes(boxes)
if type(boxes) ~= "table" or #boxes == 0 then
return false
end
for _, box in ipairs(boxes) do
if not futil.is_box(box) then
return false
end
end
return true
end
-- given a set of boxes, return a single box that covers all of them
function futil.cover_boxes(boxes)
if not futil.is_boxes(boxes) then
return { 0, 0, 0, 0, 0, 0 }
end
local cover = boxes[1]
for i = 2, #boxes do
for j = 1, 3 do
cover[j] = math.min(cover[j], boxes[i][j])
end
for j = 4, 6 do
cover[j] = math.max(cover[j], boxes[i][j])
end
end
return cover
end
--[[
for nodes:
A nodebox is defined as any of:
{
-- A normal cube; the default in most things
type = "regular"
}
{
-- A fixed box (or boxes) (facedir param2 is used, if applicable)
type = "fixed",
fixed = box OR {box1, box2, ...}
}
{
-- A variable height box (or boxes) with the top face position defined
-- by the node parameter 'leveled = ', or if 'paramtype2 == "leveled"'
-- by param2.
-- Other faces are defined by 'fixed = {}' as with 'type = "fixed"'.
type = "leveled",
fixed = box OR {box1, box2, ...}
}
{
-- A box like the selection box for torches
-- (wallmounted param2 is used, if applicable)
type = "wallmounted",
wall_top = box,
wall_bottom = box,
wall_side = box
}
{
-- A node that has optional boxes depending on neighboring nodes'
-- presence and type. See also `connects_to`.
type = "connected",
fixed = box OR {box1, box2, ...}
connect_top = box OR {box1, box2, ...}
connect_bottom = box OR {box1, box2, ...}
connect_front = box OR {box1, box2, ...}
connect_left = box OR {box1, box2, ...}
connect_back = box OR {box1, box2, ...}
connect_right = box OR {box1, box2, ...}
-- The following `disconnected_*` boxes are the opposites of the
-- `connect_*` ones above, i.e. when a node has no suitable neighbor
-- on the respective side, the corresponding disconnected box is drawn.
disconnected_top = box OR {box1, box2, ...}
disconnected_bottom = box OR {box1, box2, ...}
disconnected_front = box OR {box1, box2, ...}
disconnected_left = box OR {box1, box2, ...}
disconnected_back = box OR {box1, box2, ...}
disconnected_right = box OR {box1, box2, ...}
disconnected = box OR {box1, box2, ...} -- when there is *no* neighbor
disconnected_sides = box OR {box1, box2, ...} -- when there are *no*
-- neighbors to the sides
}
for objects:
collisionbox = { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 }, -- default
selectionbox = { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5, rotate = false },
-- { xmin, ymin, zmin, xmax, ymax, zmax } in nodes from object position.
-- Collision boxes cannot rotate, setting `rotate = true` on it has no effect.
-- If not set, the selection box copies the collision box, and will also not rotate.
-- If `rotate = false`, the selection box will not rotate with the object itself, remaining fixed to the axes.
-- If `rotate = true`, it will match the object's rotation and any attachment rotations.
-- Raycasts use the selection box and object's rotation, but do *not* obey attachment rotations
]]
futil.default_collision_box = { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 }
function futil.node_collision_box_to_object_collisionbox(collision_box)
if type(collision_box) ~= "table" then
return table.copy(futil.default_collision_box)
elseif collision_box.type == "regular" then
return table.copy(futil.default_collision_box)
elseif collision_box.type == "fixed" or collision_box.type == "leveled" or collision_box.type == "connected" then
if futil.is_box(collision_box.fixed) then
return collision_box.fixed
elseif futil.is_boxes(collision_box.fixed) then
return futil.cover_boxes(collision_box.fixed)
else
return table.copy(futil.default_collision_box)
end
elseif collision_box.type == "wallmounted" then
local boxes = {}
if collision_box.wall_top then
table.insert(boxes, collision_box.wall_top)
end
if collision_box.wall_bottom then
table.insert(boxes, collision_box.wall_bottom)
end
if collision_box.wall_side then
table.insert(boxes, collision_box.wall_side)
end
return futil.cover_boxes(boxes)
else
return table.copy(futil.default_collision_box)
end
end
function futil.node_selection_box_to_object_selectionbox(selection_box, rotate)
local selectionbox = futil.node_collision_box_to_object_collisionbox(selection_box)
selectionbox.rotate = rotate or false
return selectionbox
end

View file

@ -0,0 +1,31 @@
-- utilities to dedupe messages
local last_by_func = {}
function futil.dedupe(func, ...)
local cur = { ... }
if futil.equals(last_by_func[func], cur) then
return
end
last_by_func[func] = cur
return func(...)
end
local last_by_player_name_by_func = futil.DefaultTable(function()
return {}
end)
function futil.dedupe_by_player(func, player, ...)
local cur = { ... }
local last_by_player_name = last_by_player_name_by_func[func]
local player_name
if type(player) == "string" then
player_name = player
else
player_name = player:get_player_name()
end
if futil.equals(last_by_player_name[player_name], cur) then
return
end
last_by_player_name[player_name] = cur
return func(player, ...)
end

View file

@ -0,0 +1,111 @@
-- adapted from https://github.com/minetest/minetest/blob/master/builtin/common/misc_helpers.lua
-- but tables are sorted
local function sorter(a, b)
local ta, tb = type(a), type(b)
if ta ~= tb then
return ta < tb
end
if ta == "function" or ta == "userdata" or ta == "thread" or ta == "table" then
return tostring(a) < tostring(b)
else
return a < b
end
end
local keywords = {
["and"] = true,
["break"] = true,
["do"] = true,
["else"] = true,
["elseif"] = true,
["end"] = true,
["false"] = true,
["for"] = true,
["function"] = true,
["goto"] = true, -- Lua 5.2
["if"] = true,
["in"] = true,
["local"] = true,
["nil"] = true,
["not"] = true,
["or"] = true,
["repeat"] = true,
["return"] = true,
["then"] = true,
["true"] = true,
["until"] = true,
["while"] = true,
}
local function is_valid_identifier(str)
if not str:find("^[a-zA-Z_][a-zA-Z0-9_]*$") or keywords[str] then
return false
end
return true
end
local function basic_dump(o)
local tp = type(o)
if tp == "number" then
return tostring(o)
elseif tp == "string" then
return string.format("%q", o)
elseif tp == "boolean" then
return tostring(o)
elseif tp == "nil" then
return "nil"
-- Uncomment for full function dumping support.
-- Not currently enabled because bytecode isn't very human-readable and
-- dump's output is intended for humans.
--elseif tp == "function" then
-- return string.format("loadstring(%q)", string.dump(o))
elseif tp == "userdata" then
return tostring(o)
else
return string.format("<%s>", tp)
end
end
function futil.dump(o, indent, nested, level)
local t = type(o)
if not level and t == "userdata" then
-- when userdata (e.g. player) is passed directly, print its metatable:
return "userdata metatable: " .. futil.dump(getmetatable(o))
end
if t ~= "table" then
return basic_dump(o)
end
-- Contains table -> true/nil of currently nested tables
nested = nested or {}
if nested[o] then
return "<circular reference>"
end
nested[o] = true
indent = indent or "\t"
level = level or 1
local ret = {}
local dumped_indexes = {}
for i, v in ipairs(o) do
ret[#ret + 1] = futil.dump(v, indent, nested, level + 1)
dumped_indexes[i] = true
end
for k, v in futil.table.pairs_by_key(o, sorter) do
if not dumped_indexes[k] then
if type(k) ~= "string" or not is_valid_identifier(k) then
k = "[" .. futil.dump(k, indent, nested, level + 1) .. "]"
end
v = futil.dump(v, indent, nested, level + 1)
ret[#ret + 1] = k .. " = " .. v
end
end
nested[o] = nil
if indent ~= "" then
local indent_str = "\n" .. string.rep(indent, level)
local end_indent_str = "\n" .. string.rep(indent, level - 1)
return string.format("{%s%s%s}", indent_str, table.concat(ret, "," .. indent_str), end_indent_str)
end
return "{" .. table.concat(ret, ", ") .. "}"
end

View file

@ -0,0 +1,271 @@
local FakeInventory = futil.class1()
local function copy_list(list)
local copy = {}
for i = 1, #list do
copy[i] = ItemStack(list[i])
end
return copy
end
function FakeInventory:_init()
self._lists = {}
end
function FakeInventory.create_copy(inv)
local fake_inv = FakeInventory()
for listname, contents in pairs(inv:get_lists()) do
fake_inv:set_size(listname, inv:get_size(listname))
fake_inv:set_width(listname, inv:get_width(listname))
fake_inv:set_list(listname, contents)
end
return fake_inv
end
function FakeInventory.room_for_all(inv, listname, items)
local fake_inv = FakeInventory.create_copy(inv)
for i = 1, #items do
local item = items[i]
local remainder = fake_inv:add_item(listname, item)
if not remainder:is_empty() then
return false
end
end
return true
end
function FakeInventory:is_empty(listname)
local list = self._lists[listname]
if not list then
return true
end
for _, stack in ipairs(list) do
if not stack:is_empty() then
return false
end
end
return true
end
function FakeInventory:get_size(listname)
local list = self._lists[listname]
if not list then
return 0
end
return #list
end
function FakeInventory:set_size(listname, size)
if size == 0 then
self._lists[listname] = nil
return
end
local list = self._lists[listname] or {}
while #list < size do
list[#list + 1] = ItemStack()
end
for i = size + 1, #list do
list[i] = nil
end
self._lists[listname] = list
end
function FakeInventory:get_width(listname)
local list = self._lists[listname] or {}
return list.width or 0
end
function FakeInventory:set_width(listname, width)
local list = self._lists[listname] or {}
list.width = width
self._lists[listname] = list
end
function FakeInventory:get_stack(listname, i)
local list = self._lists[listname]
if not list or i > #list then
return ItemStack()
end
return ItemStack(list[i])
end
function FakeInventory:set_stack(listname, i, stack)
local list = self._lists[listname]
if not list or i > #list then
return
end
list[i] = ItemStack(stack)
end
function FakeInventory:get_list(listname)
local list = self._lists[listname]
if not list then
return
end
return copy_list(list)
end
function FakeInventory:set_list(listname, list)
local ourlist = self._lists[listname]
if not ourlist then
return
end
for i = 1, #ourlist do
ourlist[i] = ItemStack(list[i])
end
end
function FakeInventory:get_lists()
local lists = {}
for listname, list in pairs(self._lists) do
lists[listname] = copy_list(list)
end
return lists
end
function FakeInventory:set_lists(lists)
for listname, list in pairs(lists) do
self:set_list(listname, list)
end
end
-- add item somewhere in list, returns leftover `ItemStack`.
function FakeInventory:add_item(listname, new_item)
local list = self._lists[listname]
new_item = ItemStack(new_item)
if new_item:is_empty() or not list or #list == 0 then
return new_item
end
-- first try to find if it could be added to some existing items
for _, our_stack in ipairs(list) do
if not our_stack:is_empty() then
new_item = our_stack:add_item(new_item)
if new_item:is_empty() then
return new_item
end
end
end
-- then try to add it to empty slots
for _, our_stack in ipairs(list) do
new_item = our_stack:add_item(new_item)
if new_item:is_empty() then
break
end
end
return new_item
end
-- returns `true` if the stack of items can be fully added to the list
function FakeInventory:room_for_item(listname, stack)
local list = self._lists[listname]
if not list then
return false
end
stack = ItemStack(stack)
local copy = copy_list(list)
for _, our_stack in ipairs(copy) do
stack = our_stack:add_item(stack)
if stack:is_empty() then
break
end
end
return stack:is_empty()
end
-- take as many items as specified from the list, returns the items that were actually removed (as an `ItemStack`)
-- note that any item metadata is ignored, so attempting to remove a specific unique item this way will likely remove
-- the wrong one -- to do that use `set_stack` with an empty `ItemStack`.
function FakeInventory:remove_item(listname, stack)
local removed = ItemStack()
stack = ItemStack(stack)
local list = self._lists[listname]
if not list or stack:is_empty() then
return removed
end
local name = stack:get_name()
local count_remaining = stack:get_count()
local taken = 0
for i = #list, 1, -1 do
local our_stack = list[i]
if our_stack:get_name() == name then
local n = our_stack:take_item(count_remaining):get_count()
count_remaining = count_remaining - n
taken = taken + n
end
if count_remaining == 0 then
break
end
end
stack:set_count(taken)
return stack
end
-- returns `true` if the stack of items can be fully taken from the list.
-- If `match_meta` is false, only the items' names are compared (default: `false`).
function FakeInventory:contains_item(listname, stack, match_meta)
local list = self._lists[listname]
if not list then
return false
end
stack = ItemStack(stack)
if match_meta then
local name = stack:get_name()
local wear = stack:get_wear()
local meta = stack:get_meta()
local needed_count = stack:get_count()
for _, our_stack in ipairs(list) do
if our_stack:get_name() == name and our_stack:get_wear() == wear and our_stack:get_meta():equals(meta) then
local n = our_stack:peek_item(needed_count):get_count()
needed_count = needed_count - n
end
if needed_count == 0 then
break
end
end
return needed_count == 0
else
local name = stack:get_name()
local needed_count = stack:get_count()
for _, our_stack in ipairs(list) do
if our_stack:get_name() == name then
local n = our_stack:peek_item(needed_count):get_count()
needed_count = needed_count - n
end
if needed_count == 0 then
break
end
end
return needed_count == 0
end
end
function FakeInventory:get_location()
return {
type = "undefined",
subtype = "FakeInventory",
}
end
futil.FakeInventory = FakeInventory

View file

@ -0,0 +1,88 @@
--[[
execute the globalstep after the specified period. the actual amount of time elapsed is passed to the function,
and will always be greater than or equal to the length of the period.
futil.register_globalstep({
period = 1,
func = function(elapsed) end,
})
execute the globalstep after the specified period. if more time has elapsed than the period specified, the remainder
will be counted against the next cycle, allowing the execution to "catch up". the expected time between executions
will tend towards the specified period. IMPORTANT: do not specify a period which is less than the length of the
dedicated server step.
futil.register_globalstep({
period = 1,
catchup = "single"
func = function(period) end,
})
execute the globalstep after the specified period. if more time has elapsed than the period specified, the callback
will be executed repeatedly until the elapsed time is less than the period, and the remainder will still be counted
against the next cycle.
futil.register_globalstep({
period = 1,
catchup = "full"
func = function(period) end,
})
this is just a light wrapper over a normal minetest globalstep callback, and is only provided for completeness.
futil.register_globalstep({
func = function(dtime) end,
})
]]
local f = string.format
local dedicated_server_step = tonumber(minetest.settings:get("dedicated_server_step")) or 0.09
function futil.register_globalstep(def)
if def.period then
local elapsed = 0
if def.catchup == "full" then
assert(def.period > 0, "full catchup will cause an infinite loop if period is 0")
minetest.register_globalstep(function(dtime)
elapsed = elapsed + dtime
if elapsed < def.period then
return
end
elapsed = elapsed - def.period
def.func(def.period)
while elapsed > def.period do
elapsed = elapsed - def.period
def.func(def.period)
end
end)
elseif def.catchup == "single" or def.catchup == true then
assert(
def.period >= dedicated_server_step,
f(
"if period (%s) is less than dedicated_server_step (%s), single catchup will never fully catch up.",
def.period,
dedicated_server_step
)
)
minetest.register_globalstep(function(dtime)
elapsed = elapsed + dtime
if elapsed < def.period then
return
end
elapsed = elapsed - def.period
def.func(def.period)
end)
else
-- no catchup, just reset
minetest.register_globalstep(function(dtime)
elapsed = elapsed + dtime
if elapsed < def.period then
return
end
def.func(elapsed)
elapsed = 0
end)
end
else
-- we do nothing useful
minetest.register_globalstep(function(dtime)
def.func(dtime)
end)
end
end

View file

@ -0,0 +1,61 @@
function futil.add_groups(itemstring, new_groups)
local def = minetest.registered_items[itemstring]
if not def then
error(("attempting to override unknown item %s"):format(itemstring))
end
local groups = table.copy(def.groups or {})
futil.table.set_all(groups, new_groups)
minetest.override_item(itemstring, { groups = groups })
end
function futil.remove_groups(itemstring, ...)
local def = minetest.registered_items[itemstring]
if not def then
error(("attempting to override unknown item %s"):format(itemstring))
end
local groups = table.copy(def.groups or {})
for _, group in ipairs({ ... }) do
groups[group] = nil
end
minetest.override_item(itemstring, { groups = groups })
end
function futil.get_items_with_group(group)
if futil.items_by_group then
return futil.items_by_group[group] or {}
end
local items = {}
for item in pairs(minetest.registered_items) do
if minetest.get_item_group(item, group) > 0 then
table.insert(items, item)
end
end
return items
end
function futil.get_item_with_group(group)
return futil.get_items_with_group(group)[1]
end
function futil.generate_items_by_group()
local items_by_group = {}
for item, def in pairs(minetest.registered_items) do
for group in pairs(def.groups or {}) do
local items = items_by_group[group] or {}
table.insert(items, item)
items_by_group[group] = items
end
end
futil.items_by_group = items_by_group
end
if INIT == "game" then
-- it's not 100% safe to assume items and groups can't change after this point.
-- but please, don't do that :\
minetest.register_on_mods_loaded(futil.generate_items_by_group)
end

View file

@ -0,0 +1,100 @@
local f = string.format
local current_id = 0
local function get_next_id()
current_id = current_id + 1
return current_id
end
local EphemeralHud = futil.class1()
function EphemeralHud:_init(player, hud_def)
self._player_name = player:get_player_name()
if (hud_def.type or hud_def.hud_elem_type) == "waypoint" then
self._id_field = "text2"
else
self._id_field = "name"
end
self._id = f("ephemeral_hud:%s:%i", hud_def[self._id_field] or "", get_next_id())
hud_def[self._id_field] = self._id
self._hud_id = player:hud_add(hud_def)
end
function EphemeralHud:is_active()
if not self._hud_id then
return false
end
local player = minetest.get_player_by_name(self._player_name)
if not player then
self._hud_id = nil
return false
end
local hud_def = player:hud_get(self._hud_id)
if not hud_def then
self._hud_id = nil
return false
end
if hud_def[self._id_field] ~= self._id then
self._hud_id = nil
return false
end
return true
end
function EphemeralHud:change(new_hud_def)
if not self:is_active() then
futil.log("warning", "[ephemeral hud] cannot update an inactive hud")
return false
end
local player = minetest.get_player_by_name(self._player_name)
local old_hud_def = player:hud_get(self._hud_id)
for key, value in pairs(new_hud_def) do
if key == "hud_elem_type" then
if value ~= (old_hud_def.type or old_hud_def.hud_elem_type) then
error("cannot change hud_elem_type")
end
elseif key == "type" then
if value ~= (old_hud_def.type or old_hud_def.hud_elem_type) then
error("cannot change type")
end
elseif key == self._id_field then
if value ~= self._id then
error(f("cannot change the value of %q, as this is an ID", self._id_field))
end
else
if key == "position" or key == "scale" or key == "align" or key == "offset" then
value = futil.vector.v2f_to_float_32(value)
end
if not futil.equals(old_hud_def[key], value) then
player:hud_change(self._hud_id, key, value)
end
end
end
return true
end
function EphemeralHud:remove()
if not self:is_active() then
futil.log("warning", "[ephemeral hud] cannot remove an inactive hud")
return false
end
local player = minetest.get_player_by_name(self._player_name)
player:hud_remove(self._hud_id)
self._hud_id = nil
end
futil.EphemeralHud = EphemeralHud
-- note: sometimes HUDs can fail to get created. if so, the HUD object returned here will be "inactive".
function futil.create_ephemeral_hud(player, timeout, hud_def)
local hud = EphemeralHud(player, hud_def)
minetest.after(timeout, function()
if hud:is_active() then
hud:remove()
end
end)
return hud
end

View file

@ -0,0 +1,172 @@
--[[
local my_hud = futil.define_hud("my_mod:my_hud", {
period = 1,
catchup = nil, -- not currently supported
name_field = nil, -- in case you want to override the id field
enabled_by_default = nil, -- set to true to enable by default
get_hud_data = function()
-- get data that's identical for all players
-- passed to get_hud_def
end,
get_hud_def = function(player, data)
return {}
end,
})
my_hud:toggle_enabled(player)
]]
local f = string.format
local ManagedHud = futil.class1()
function ManagedHud:_init(hud_name, def)
self.name = hud_name
self._name_field = def.name_field or ((def.type or def.hud_elem_type) == "waypoint" and "text2" or "name")
self._period = def.period
self._get_hud_data = def.get_hud_data
self._get_hud_def = def.get_hud_def
self._enabled_by_default = def.enabled_by_default
self._hud_id_by_player_name = {}
self._hud_enabled_key = f("hud_manager:%s_enabled", hud_name)
self._hud_name = f("hud_manager:%s", hud_name)
end
function ManagedHud:is_enabled(player)
local meta = player:get_meta()
local value = meta:get(self._hud_enabled_key)
if value == nil then
return self._enabled_by_default
else
return minetest.is_yes(value)
end
end
function ManagedHud:set_enabled(player, value)
local meta = player:get_meta()
if minetest.is_yes(value) then
meta:set_string(self._hud_enabled_key, "y")
else
meta:set_string(self._hud_enabled_key, "n")
end
end
function ManagedHud:toggle_enabled(player)
local meta = player:get_meta()
local enabled = not self:is_enabled(player)
if enabled then
meta:set_string(self._hud_enabled_key, "y")
else
meta:set_string(self._hud_enabled_key, "n")
end
return enabled
end
function ManagedHud:update(player, data)
local is_enabled = self:is_enabled(player)
local player_name = player:get_player_name()
local hud_id = self._hud_id_by_player_name[player_name]
local old_hud_def
if hud_id then
old_hud_def = player:hud_get(hud_id)
if old_hud_def and old_hud_def[self._name_field] == self._hud_name then
if not is_enabled then
player:hud_remove(hud_id)
self._hud_id_by_player_name[player_name] = nil
return
end
else
-- hud_id is bad
hud_id = nil
old_hud_def = nil
end
end
if is_enabled then
local new_hud_def = self._get_hud_def(player, data)
if not new_hud_def then
if hud_id then
player:hud_remove(hud_id)
self._hud_id_by_player_name[player_name] = nil
end
return
elseif new_hud_def[self._name_field] and new_hud_def[self._name_field] ~= self._hud_name then
error(f("you cannot specify the value of the %q field, this is generated", self._name_field))
end
if old_hud_def then
for k, v in pairs(new_hud_def) do
if k == "position" or k == "scale" or k == "align" or k == "offset" then
v = futil.vector.v2f_to_float_32(v)
end
if not futil.equals(old_hud_def[k], v) and k ~= "type" and k ~= "hud_elem_type" then
player:hud_change(hud_id, k, v)
end
end
else
new_hud_def[self._name_field] = self._hud_name
hud_id = player:hud_add(new_hud_def)
end
end
self._hud_id_by_player_name[player_name] = hud_id
end
futil.defined_huds = {}
function futil.define_hud(hud_name, def)
if futil.defined_huds[hud_name] then
error(f("hud %s already exists", hud_name))
end
local hud = ManagedHud(hud_name, def)
futil.defined_huds[hud_name] = hud
return hud
end
-- TODO: register_hud instead of define_hud, plus alias the old
local function update_hud(hud, players)
local data
if hud._get_hud_data then
local is_any_enabled = false
for i = 1, #players do
if hud:is_enabled(players[i]) then
is_any_enabled = true
break
end
end
if is_any_enabled then
data = hud._get_hud_data()
end
end
for i = 1, #players do
hud:update(players[i], data)
end
end
-- TODO refactor to use futil.register_globalstep for each hud, to allow use of catchup mechanics
-- ... why would HUD updates need catchup mechanics?
local elapsed_by_hud_name = {}
minetest.register_globalstep(function(dtime)
local players = minetest.get_connected_players()
if #players == 0 then
return
end
for hud_name, hud in pairs(futil.defined_huds) do
if hud._period then
local elapsed = (elapsed_by_hud_name[hud_name] or 0) + dtime
if elapsed < hud._period then
elapsed_by_hud_name[hud_name] = elapsed
else
elapsed_by_hud_name[hud_name] = 0
update_hud(hud, players)
end
else
update_hud(hud, players)
end
end
end)

View file

@ -0,0 +1,171 @@
local f = string.format
local function is_vertical_frames(animation)
return (animation.type == "vertical_frames" and animation.aspect_w and animation.aspect_h)
end
local function get_single_frame(animation, image_name)
return ("[combine:%ix%i^[noalpha^[colorize:#FFF:255^[mask:%s"):format(
animation.aspect_w,
animation.aspect_h,
image_name
)
end
local function is_sheet_2d(animation)
return (animation.type == "sheet_2d" and animation.frames_w and animation.frames_h)
end
local function get_sheet_2d(animation, image_name)
return ("%s^[sheet:%ix%i:0,0"):format(image_name, animation.frames_w, animation.frames_h)
end
local get_image_from_tile = futil.memoize1(function(tile)
if type(tile) == "string" then
return tile
elseif type(tile) == "table" then
local image_name
if type(tile.image) == "string" then
image_name = tile.image
elseif type(tile.name) == "string" then
image_name = tile.name
end
if image_name then
local animation = tile.animation
if animation then
if is_vertical_frames(animation) then
return get_single_frame(animation, image_name)
elseif is_sheet_2d(animation) then
return get_sheet_2d(animation, image_name)
end
end
return image_name
end
end
return "unknown_node.png"
end)
local function get_image_cube(tiles)
if #tiles >= 6 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[6] or "no_texture.png"),
get_image_from_tile(tiles[3] or "no_texture.png")
)
elseif #tiles == 5 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[5] or "no_texture.png"),
get_image_from_tile(tiles[3] or "no_texture.png")
)
elseif #tiles == 4 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[4] or "no_texture.png"),
get_image_from_tile(tiles[3] or "no_texture.png")
)
elseif #tiles == 3 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[3] or "no_texture.png"),
get_image_from_tile(tiles[3] or "no_texture.png")
)
elseif #tiles == 2 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[2] or "no_texture.png"),
get_image_from_tile(tiles[2] or "no_texture.png")
)
elseif #tiles == 1 then
return minetest.inventorycube(
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[1] or "no_texture.png"),
get_image_from_tile(tiles[1] or "no_texture.png")
)
end
return "no_texture.png"
end
local function is_normal_node(drawtype)
return (
drawtype == "normal"
or drawtype == "allfaces"
or drawtype == "allfaces_optional"
or drawtype == "glasslike"
or drawtype == "glasslike_framed"
or drawtype == "glasslike_framed_optional"
or drawtype == "liquid"
)
end
local cache = {}
function futil.get_wield_image(item)
if type(item) == "string" then
item = ItemStack(item)
end
if item:is_empty() then
return "blank.png"
end
local def = item:get_definition()
if not def then
return "unknown_item.png"
end
local itemstring = item:to_string()
local cached = cache[itemstring]
if cached then
return cached
end
local meta = item:get_meta()
local color = meta:get("color") or def.color
local image = "no_texture.png"
if def.wield_image and def.wield_image ~= "" then
local parts = { def.wield_image }
if color then
parts[#parts + 1] = f("[colorize:%s:alpha", futil.escape_texture(color))
end
if def.wield_overlay then
parts[#parts + 1] = def.wield_overlay
end
image = table.concat(parts, "^")
elseif def.inventory_image and def.inventory_image ~= "" then
local parts = { def.inventory_image }
if color then
parts[#parts + 1] = f("[colorize:%s:alpha", futil.escape_texture(color))
end
if def.inventory_overlay then
parts[#parts + 1] = def.inventory_overlay
end
image = table.concat(parts, "^")
elseif def.type == "node" then
if def.drawtype == "nodebox" or def.drawtype == "mesh" then
image = "no_texture.png"
else
local tiles = def.tiles
if type(tiles) == "string" then
image = get_image_from_tile(tiles)
elseif type(tiles) == "table" then
if is_normal_node(def.drawtype) then
image = get_image_cube(tiles)
else
image = get_image_from_tile(tiles[1])
end
end
end
end
cache[itemstring] = image
return image
end

View file

@ -0,0 +1,24 @@
futil.dofile("minetest", "box")
futil.dofile("minetest", "dedupe")
futil.dofile("minetest", "dump")
futil.dofile("minetest", "fake_inventory")
futil.dofile("minetest", "group")
futil.dofile("minetest", "image")
futil.dofile("minetest", "item")
futil.dofile("minetest", "registration")
futil.dofile("minetest", "serialization")
futil.dofile("minetest", "set_look_dir")
futil.dofile("minetest", "strip_translation")
futil.dofile("minetest", "texture")
futil.dofile("minetest", "time")
futil.dofile("minetest", "vector")
if INIT == "game" then
futil.dofile("minetest", "globalstep")
futil.dofile("minetest", "hud_ephemeral")
futil.dofile("minetest", "hud_manager")
futil.dofile("minetest", "inventory")
futil.dofile("minetest", "object")
futil.dofile("minetest", "object_properties")
futil.dofile("minetest", "raycast")
end

View file

@ -0,0 +1,40 @@
function futil.get_location_string(inv)
local location = inv:get_location()
if location.type == "node" then
return ("nodemeta:%i,%i,%i"):format(location.pos.x, location.pos.y, location.pos.z)
elseif location.type == "player" then
return ("player:%s"):format(location.name)
elseif location.type == "detached" then
return ("detached:%s"):format(location.name)
else
error(("unexpected location? %s"):format(dump(location)))
end
end
-- InvRef:remove_item() ignores metadata, and sometimes that's wrong
-- for logic, see InventoryList::removeItem in inventory.cpp
function futil.remove_item_with_meta(inv, listname, itemstack)
itemstack = ItemStack(itemstack)
if itemstack:is_empty() then
return ItemStack()
end
local removed = ItemStack()
for i = 1, inv:get_size(listname) do
local invstack = inv:get_stack(listname, i)
if
invstack:get_name() == itemstack:get_name()
and invstack:get_wear() == itemstack:get_wear()
and invstack:get_meta() == itemstack:get_meta()
then
local still_to_remove = itemstack:get_count() - removed:get_count()
local leftover = removed:add_item(invstack:take_item(still_to_remove))
-- if we've requested to remove more than the stack size, ignore the limit
removed:set_count(removed:get_count() + leftover:get_count())
inv:set_stack(listname, i, invstack)
if removed:get_count() == itemstack:get_count() then
break
end
end
end
return removed
end

View file

@ -0,0 +1,133 @@
local f = string.format
-- if allow_unregistered is false or absent, if the original item or its alias is not a registered item, will return nil
function futil.resolve_item(item_or_string, allow_unregistered)
local item_stack = ItemStack(item_or_string)
local name = item_stack:get_name()
local seen = { [name] = true }
local alias = minetest.registered_aliases[name]
while alias do
name = alias
seen[name] = true
alias = minetest.registered_aliases[name]
if seen[alias] then
error(f("alias cycle on %s", name))
end
end
if minetest.registered_items[name] or allow_unregistered then
item_stack:set_name(name)
return item_stack:to_string()
end
end
function futil.resolve_itemstack(item_or_string)
return ItemStack(futil.resolve_item(item_or_string, true))
end
if ItemStack().equals then
-- https://github.com/minetest/minetest/pull/12771
function futil.items_equals(item1, item2)
item1 = type(item1) == "userdata" and item1 or ItemStack(item1)
item2 = type(item2) == "userdata" and item2 or ItemStack(item2)
return item1 == item2
end
else
local equals = futil.equals
function futil.items_equals(item1, item2)
item1 = type(item1) == "userdata" and item1 or ItemStack(item1)
item2 = type(item2) == "userdata" and item2 or ItemStack(item2)
return equals(item1:to_table(), item2:to_table())
end
end
-- TODO: probably this should have a 3nd argument to handle tool and tool_group stuff
function futil.get_primary_drop(stack, filter)
stack = ItemStack(stack)
local name = stack:get_name()
local meta = stack:get_meta()
local palette_index = tonumber(meta:get_int("palette_index"))
local def = stack:get_definition()
if palette_index then
-- https://github.com/mt-mods/unifieddyes/blob/36c8bb5f5b8a0485225d2547c8978291ff710291/api.lua#L70-L90
local del_color
if def.paramtype2 == "color" and palette_index == 240 and def.palette == "unifieddyes_palette_extended.png" then
del_color = true
elseif
def.paramtype2 == "colorwallmounted"
and palette_index == 0
and def.palette == "unifieddyes_palette_colorwallmounted.png"
then
del_color = true
elseif
def.paramtype2 == "colorfacedir"
and palette_index == 0
and string.find(def.palette, "unifieddyes_palette_")
then
del_color = true
end
if del_color then
meta:set_string("palette_index", "")
palette_index = nil
end
end
local drop = def.drop
if drop == nil then
stack:set_count(1)
return stack
elseif drop == "" then
return nil
elseif type(drop) == "string" then
drop = ItemStack(drop)
drop:set_count(1)
return drop
elseif type(drop) == "table" then
local most_common_item
local inherit_color = false
local rarity = math.huge
if not drop.items then
error(f("unexpected drop table for %s: %s", stack:to_string(), dump(drop)))
end
for _, items in ipairs(drop.items) do
if (items.rarity or 1) < rarity then
for item in ipairs(items.items) do
if (not filter) or filter(item) then
most_common_item = item
inherit_color = items.inherit_color or false
rarity = items.rarity
break
end
end
end
end
if not most_common_item then
return
end
most_common_item = ItemStack(most_common_item)
most_common_item:set_count(1)
if inherit_color and palette_index then
local meta2 = most_common_item:get_meta()
meta2:set_int("palette_index", palette_index)
end
return most_common_item
else
error(f("invalid drop of %s? %q", dump(name, drop)))
end
end

View file

@ -0,0 +1,200 @@
local v_new = vector.new
-- if object is attached, get the velocity of the object it is attached to
function futil.get_velocity(object)
local parent = object:get_attach()
while parent do
object = parent
parent = object:get_attach()
end
return object:get_velocity()
end
function futil.get_horizontal_speed(object)
local velocity = futil.get_velocity(object)
velocity.y = 0
return vector.length(velocity)
end
local function insert_connected(boxes, something)
if futil.is_box(something) then
table.insert(boxes, something)
elseif futil.is_boxes(something) then
table.insert_all(boxes, something)
end
end
local function get_boxes(cb)
if not cb then
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
if cb.type == "regular" then
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
elseif cb.type == "fixed" then
if futil.is_box(cb.fixed) then
return { cb.fixed }
elseif futil.is_boxes(cb.fixed) then
return cb.fixed
else
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
elseif cb.type == "leveled" then
-- TODO: have to check param2
if futil.is_box(cb.fixed) then
return { cb.fixed }
elseif futil.is_boxes(cb.fixed) then
return cb.fixed
else
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
elseif cb.type == "wallmounted" then
-- TODO: have to check param2? or?
local boxes = {}
if futil.is_box(cb.wall_top) then
table.insert(boxes, cb.wall_top)
end
if futil.is_box(cb.wall_bottom) then
table.insert(boxes, cb.wall_bottom)
end
if futil.is_box(cb.wall_side) then
table.insert(boxes, cb.wall_side)
end
if #boxes > 0 then
return boxes
else
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
elseif cb.type == "connected" then
-- TODO: very very complicated to check, just fudge and add everything
local boxes = {}
insert_connected(boxes, cb.fixed)
insert_connected(boxes, cb.connect_top)
insert_connected(boxes, cb.connect_bottom)
insert_connected(boxes, cb.connect_front)
insert_connected(boxes, cb.connect_left)
insert_connected(boxes, cb.connect_back)
insert_connected(boxes, cb.connect_right)
insert_connected(boxes, cb.disconnected_top)
insert_connected(boxes, cb.disconnected_bottom)
insert_connected(boxes, cb.disconnected_front)
insert_connected(boxes, cb.disconnected_left)
insert_connected(boxes, cb.disconnected_back)
insert_connected(boxes, cb.disconnected_right)
insert_connected(boxes, cb.disconnected)
insert_connected(boxes, cb.disconnected_sides)
if #boxes > 0 then
return boxes
else
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
end
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
local function get_collision_boxes(node)
local node_def = minetest.registered_nodes[node.name]
if not node_def then
-- unknown nodes are regular solid nodes
return { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
if not node_def.walkable then
return {}
end
local boxes
if node_def.collision_box then
boxes = get_boxes(node_def.collision_box)
elseif node_def.drawtype == "nodebox" then
boxes = get_boxes(node_def.node_box)
else
boxes = { { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 } }
end
--[[
if node_def.paramtype2 == "facedir" then
-- TODO: re-orient boxes
end
]]
return boxes
end
local function is_pos_on_ground(feet_pos, player_box)
local node = minetest.get_node(feet_pos)
local node_boxes = get_collision_boxes(node)
for _, node_box in ipairs(node_boxes) do
local actual_node_box = futil.box_offset(node_box, feet_pos)
if futil.boxes_intersect(actual_node_box, player_box) then
return true
end
end
return false
end
function futil.is_on_ground(player)
local p_pos = player:get_pos()
local cb = player:get_properties().collisionbox
-- collect the positions of the nodes below the player's feet
local feet_poss = {
v_new(math.round(p_pos.x + cb[1]), math.ceil(p_pos.y + cb[2] - 0.5), math.round(p_pos.z + cb[3])),
v_new(math.round(p_pos.x + cb[1]), math.ceil(p_pos.y + cb[2] - 0.5), math.round(p_pos.z + cb[6])),
v_new(math.round(p_pos.x + cb[4]), math.ceil(p_pos.y + cb[2] - 0.5), math.round(p_pos.z + cb[3])),
v_new(math.round(p_pos.x + cb[4]), math.ceil(p_pos.y + cb[2] - 0.5), math.round(p_pos.z + cb[6])),
}
for _, feet_pos in ipairs(feet_poss) do
if is_pos_on_ground(feet_pos, futil.box_offset(cb, p_pos)) then
return true
end
end
return false
end
function futil.get_object_center(object)
local pos = object:get_pos()
if not pos then
return
end
local cb = object:get_properties().collisionbox
return v_new(pos.x + (cb[1] + cb[4]) / 2, pos.y + (cb[2] + cb[5]) / 2, pos.z + (cb[3] + cb[6]) / 2)
end
function futil.is_player(obj)
return minetest.is_player(obj) and not obj.is_fake_player
end
function futil.is_valid_object(obj)
return obj and type(obj.get_pos) == "function" and vector.check(obj:get_pos())
end
-- this is meant to be able to get the HP of any object, including "immortal" ones whose health is managed themselves
-- it is *NOT* complete - i've got no idea where every mob API stores its hp.
-- "health" is mobs_redo (which is actually redundant with `:get_hp()` because they're not actually immortal.
-- "hp" is mobkit (and petz, which comes with its own fork of mobkit), and also creatura.
function futil.get_hp(obj)
if not futil.is_valid_object(obj) then
-- not an object or dead
return 0
end
local ent = obj:get_luaentity()
if ent and (type(ent.hp) == "number" or type(ent.health) == "number") then
return ent.hp or ent.health
end
local armor_groups = obj:get_armor_groups()
if (armor_groups["immortal"] or 0) == 0 then
return obj:get_hp()
end
return math.huge -- presumably actually immortal
end

View file

@ -0,0 +1,158 @@
local f = string.format
local iall = futil.functional.iall
local map = futil.map
local in_bounds = futil.math.in_bounds
local is_integer = futil.math.is_integer
local is_number = futil.is_number
local is_string = futil.is_string
local is_table = futil.is_table
local function valid_box(value)
if value == nil then
return true
elseif not is_table(value) then
return false
elseif #value ~= 6 then
return false
else
return iall(map(is_number, value))
end
end
local function valid_visual_size(value)
if not is_table(value) then
return false
end
local z_type = type(value.z)
return is_number(value.x) and is_integer(value.y) and (z_type == "number" or z_type == nil)
end
local function valid_textures(value)
if not is_table(value) then
return false
end
return iall(map(is_string, value))
end
local function valid_color_spec(value)
local t = type(value)
if t == "string" then
-- TODO: we could check for valid values, but that's ... tedious
return true
elseif t == "table" then
local is_number_ = is_number
local is_integer_ = is_integer
local in_bounds_ = in_bounds
local x = value.x
local y = value.y
local z = value.z
local a = value.a
return (
is_number_(x)
and in_bounds_(0, x, 255)
and is_integer_(x)
and is_number_(y)
and in_bounds_(0, y, 255)
and is_integer_(y)
and is_number_(z)
and in_bounds_(0, z, 255)
and is_integer_(z)
and (a == nil or (is_number_(a) and in_bounds_(0, a, 255) and is_integer_(a)))
)
end
return false
end
local function valid_colors(value)
if not is_table(value) then
return false
end
return iall(map(valid_color_spec, value))
end
local function valid_spritediv(value)
if not is_table(value) then
return false
end
local x = value.x
local y = value.y
return is_number(x) and is_integer(x) and is_number(y) and is_number(y)
end
local function valid_automatic_face_movement_dir(value)
return value == false or is_number(value)
end
local function valid_hp_max(value)
return is_number(value) and is_integer(value) and in_bounds(1, value, 65535)
end
local object_property = {
visual = "string",
visual_size = valid_visual_size,
mesh = "string",
textures = valid_textures,
colors = valid_colors,
use_texture_alpha = "boolean",
spritediv = valid_spritediv,
initial_sprite_basepos = valid_spritediv,
is_visible = "boolean",
automatic_rotate = "number",
automatic_face_movement_dir = valid_automatic_face_movement_dir,
automatic_face_movement_max_rotation_per_sec = "number",
backface_culling = "number",
glow = "number",
damage_texture_modifier = "string",
shaded = "boolean",
hp_max = valid_hp_max,
physical = "boolean",
pointable = "boolean",
collide_with_objects = "boolean",
collisionbox = valid_box,
selectionbox = valid_box,
makes_footstep_sound = "boolean",
stepheight = "number",
nametag = "string",
nametag_color = valid_color_spec,
nametag_bgcolor = valid_color_spec,
infotext = "string",
static_save = "boolean",
show_on_minimap = "boolean",
}
function futil.is_property_key(key)
return object_property[key] ~= nil
end
function futil.is_valid_property_value(key, value)
local kind = object_property[key]
if not kind then
return false
end
if type(kind) == "string" then
return type(value) == kind
elseif type(kind) == "function" then
return kind(value)
else
error(f("coding error in futil for key %q", key))
end
end

View file

@ -0,0 +1,30 @@
-- before 5.9, raycasts can miss objects they should hit if the cast is too short
-- see https://github.com/minetest/minetest/issues/14337
function futil.safecast(start, stop, objects, liquids, margin)
margin = margin or 5
local ray = stop - start
local ray_length = ray:length()
if ray_length == 0 then
return function() end
elseif ray_length >= margin then
return Raycast(start, stop, objects, liquids)
end
local actual_stop = start + ray:normalize() * margin
local raycast = Raycast(start, actual_stop, objects, liquids)
local stopped = false
return function()
if stopped then
return
end
local pt = raycast()
if pt then
local ip = pt.intersection_point
if (ip - start):length() > ray_length then
stopped = true
return
end
return pt
end
end
end

View file

@ -0,0 +1,15 @@
function futil.make_registration()
local t = {}
local registerfunc = function(func)
t[#t + 1] = func
end
return t, registerfunc
end
function futil.make_registration_reverse()
local t = {}
local registerfunc = function(func)
table.insert(t, 1, func)
end
return t, registerfunc
end

View file

@ -0,0 +1,87 @@
local f = string.format
local deserialize = minetest.deserialize
local pairs_by_key = futil.table.pairs_by_key
function futil.serialize(x)
if type(x) == "number" or type(x) == "boolean" or type(x) == "nil" then
return tostring(x)
elseif type(x) == "string" then
return f("%q", x)
elseif type(x) == "table" then
local parts = {}
for k, v in pairs_by_key(x) do
table.insert(parts, f("[%s] = %s", futil.serialize(k), futil.serialize(v)))
end
return f("{%s}", table.concat(parts, ", "))
else
error(f("can't serialize type %s", type(x)))
end
end
function futil.deserialize(data)
return deserialize(f("return %s", data))
end
function futil.serialize_invlist(inv, listname)
local itemstrings = {}
local list = inv:get_list(listname)
if not list then
error(f("couldn't find list %s of %s", listname, minetest.write_json(inv:get_location())))
end
for _, stack in ipairs(list) do
table.insert(itemstrings, stack:to_string())
end
return futil.serialize(itemstrings)
end
function futil.deserialize_invlist(serialized_list, inv, listname)
if not inv:is_empty(listname) then
error(("trying to deserialize into a non-empty list %s (%s)"):format(listname, serialized_list))
end
local itemstrings = futil.deserialize(serialized_list) or minetest.parse_json(serialized_list)
inv:set_size(listname, #itemstrings)
for i, itemstring in ipairs(itemstrings) do
inv:set_stack(listname, i, ItemStack(itemstring))
end
end
function futil.serialize_inv(inv)
local serialized_lists = {}
for listname in pairs(inv:get_lists()) do
serialized_lists[listname] = futil.serialize_invlist(inv, listname)
end
return futil.serialize(serialized_lists)
end
function futil.deserialize_inv(serialized_lists, inv)
for listname, serialized_list in pairs(futil.deserialize(serialized_lists)) do
futil.deserialize_invlist(serialized_list, inv, listname)
end
end
function futil.serialize_node_meta(pos)
local meta = minetest.get_meta(pos)
local inv = meta:get_inventory()
return futil.serialize({
fields = meta:to_table().fields,
inventory = futil.serialize_inv(inv),
})
end
function futil.deserialize_node_meta(serialized_node_meta, pos)
local meta = minetest.get_meta(pos)
local x = futil.deserialize(serialized_node_meta)
meta:from_table({ fields = x.fields })
local inv = meta:get_inventory()
futil.deserialize_inv(x.inventory, inv)
end

View file

@ -0,0 +1,7 @@
local pi = math.pi
function futil.set_look_dir(player, look_dir)
local pitch = math.asin(-look_dir.y)
local yaw = math.atan2(look_dir.z, look_dir.x)
player:set_look_vertical(pitch)
player:set_look_horizontal((yaw + 1.5 * pi) % (2.0 * pi))
end

View file

@ -0,0 +1,205 @@
local function tokenize(s)
local tokens = {}
local i = 1
local j = 1
while true do
if s:sub(j, j) == "" then
if i < j then
table.insert(tokens, s:sub(i, j - 1))
end
return tokens
elseif s:sub(j, j):byte() == 27 then
if i < j then
table.insert(tokens, s:sub(i, j - 1))
end
i = j
local n = s:sub(i + 1, i + 1)
if n == "(" then
local m = s:sub(i + 2, i + 2)
local k = s:find(")", i + 3, true)
if not k then
futil.log("error", "strip_translation: couldn't tokenize %q", s)
return {}
end
if m == "T" then
table.insert(tokens, {
type = "translation",
domain = s:sub(i + 4, k - 1),
})
elseif m == "c" then
table.insert(tokens, {
type = "color",
color = s:sub(i + 4, k - 1),
})
elseif m == "b" then
table.insert(tokens, {
type = "bgcolor",
color = s:sub(i + 4, k - 1),
})
else
futil.log("error", "strip_translation: couldn't tokenize %q", s)
return {}
end
i = k + 1
j = k + 1
elseif n == "F" then
table.insert(tokens, {
type = "start",
})
i = j + 2
j = j + 2
elseif n == "E" then
table.insert(tokens, {
type = "stop",
})
i = j + 2
j = j + 2
else
futil.log("error", "strip_translation: couldn't tokenize %q", s)
return {}
end
else
j = j + 1
end
end
end
local function parse(tokens, i, parsed)
parsed = parsed or {}
i = i or 1
while i <= #tokens do
local token = tokens[i]
if type(token) == "string" then
table.insert(parsed, token)
i = i + 1
elseif token.type == "color" or token.type == "bgcolor" then
table.insert(parsed, token)
i = i + 1
elseif token.type == "translation" then
local contents = {
type = "translation",
domain = token.domain,
}
i = i + 1
contents, i = parse(tokens, i, contents)
if i == -1 then
return "", -1
end
table.insert(parsed, contents)
elseif token.type == "start" then
local contents = {
type = "escape",
}
i = i + 1
contents, i = parse(tokens, i, contents)
if i == -1 then
return "", -1
end
table.insert(parsed, contents)
elseif token.type == "stop" then
i = i + 1
return parsed, i
else
futil.log("error", "strip_translation: couldn't parse %s", dump(token):gsub("%s+", ""))
return "", -1
end
end
return parsed, i
end
local function unparse_and_strip_translation(parsed, parts)
parts = parts or {}
for _, part in ipairs(parsed) do
if type(part) == "string" then
table.insert(parts, part)
else
if part.type == "bgcolor" then
table.insert(parts, ("\27(b@%s)"):format(part.color))
elseif part.type == "color" then
table.insert(parts, ("\27(c@%s)"):format(part.color))
elseif part.domain then
unparse_and_strip_translation(part, parts)
else
unparse_and_strip_translation(part, parts)
end
end
end
return parts
end
local function erase_after_newline(parsed, erasing)
local single_line_parsed = {}
for _, piece in ipairs(parsed) do
if type(piece) == "string" then
if not erasing then
if piece:find("\n") then
erasing = true
local single_line = piece:match("^([^\n]*)\n")
table.insert(single_line_parsed, single_line)
else
table.insert(single_line_parsed, piece)
end
end
elseif piece.type == "bgcolor" or piece.type == "color" then
table.insert(single_line_parsed, piece)
elseif piece.type == "escape" then
table.insert(single_line_parsed, erase_after_newline(piece, erasing))
elseif piece.type == "translation" then
local stuff = erase_after_newline(piece, erasing)
stuff.domain = piece.domain
table.insert(single_line_parsed, stuff)
else
futil.log("error", "strip_translation: couldn't erase_after_newline %s", dump(parsed):gsub("%s+", ""))
return {}
end
end
return single_line_parsed
end
local function unparse(parsed, parts)
parts = parts or {}
for _, part in ipairs(parsed) do
if type(part) == "string" then
table.insert(parts, part)
else
if part.type == "bgcolor" then
table.insert(parts, ("\27(b@%s)"):format(part.color))
elseif part.type == "color" then
table.insert(parts, ("\27(c@%s)"):format(part.color))
elseif part.domain then
table.insert(parts, ("\27(T@%s)"):format(part.domain))
unparse(part, parts)
table.insert(parts, "\27E")
else
table.insert(parts, "\27F")
unparse(part, parts)
table.insert(parts, "\27E")
end
end
end
return parts
end
function futil.strip_translation(msg)
local tokens = tokenize(msg)
local parsed = parse(tokens)
return table.concat(unparse_and_strip_translation(parsed), "")
end
function futil.get_safe_short_description(item)
item = type(item) == "userdata" and item or ItemStack(item)
local description = item:get_description()
local tokens = tokenize(description)
local parsed = parse(tokens)
local single_line_parsed = erase_after_newline(parsed)
local single_line = table.concat(unparse(single_line_parsed), "")
return single_line
end

View file

@ -0,0 +1,9 @@
-- https://github.com/minetest/minetest/blob/9fc018ded10225589d2559d24a5db739e891fb31/doc/lua_api.txt#L453-L462
function futil.escape_texture(texturestring)
-- store in a variable so we don't return both rvs of gsub
local v = texturestring:gsub("[%^:]", {
["^"] = "\\^",
[":"] = "\\:",
})
return v
end

View file

@ -0,0 +1,7 @@
function futil.wait(us)
local wait_until = minetest.get_us_time() + us
local get_us_time = minetest.get_us_time
while get_us_time() < wait_until do
-- the NOTHING function does nothing.
end
end

View file

@ -0,0 +1,402 @@
local m_abs = math.abs
local m_acos = math.acos
local m_asin = math.asin
local m_atan2 = math.atan2
local m_cos = math.cos
local m_floor = math.floor
local m_min = math.min
local m_max = math.max
local m_pi = math.pi
local m_pow = math.pow
local m_random = math.random
local m_sin = math.sin
local v_add = vector.add
local v_new = vector.new
local v_sort = vector.sort
local v_sub = vector.subtract
local in_bounds = futil.math.in_bounds
local bound = futil.math.bound
local mapblock_size = 16 -- can be redefined, but effectively hard-coded
local chunksize = m_floor(tonumber(minetest.settings:get("chunksize")) or 5) -- # of mapblocks in a chunk (1 dim)
local chunksize_nodes = mapblock_size * chunksize -- # of nodes in a chunk (1 dim)
local max_mapgen_limit = 31007 -- hard coded
local mapgen_limit =
bound(0, m_floor(tonumber(minetest.settings:get("mapgen_limit")) or max_mapgen_limit), max_mapgen_limit)
local mapgen_limit_b = m_floor(mapgen_limit / mapblock_size) -- # of mapblocks
-- *actual* minimum and maximum coordinates - one mapblock short of the theoretical min and max
local map_min_i = (-mapgen_limit_b * mapblock_size) + chunksize_nodes
local map_max_i = ((mapgen_limit_b + 1) * mapblock_size - 1) - chunksize_nodes
local map_min_p = v_new(map_min_i, map_min_i, map_min_i)
local map_max_p = v_new(map_max_i, map_max_i, map_max_i)
futil.vector = {}
function futil.vector.get_bounds(pos, radius)
return v_sub(pos, radius), v_add(pos, radius)
end
futil.get_bounds = futil.vector.get_bounds
function futil.vector.get_world_bounds()
return map_min_p, map_max_p
end
futil.get_world_bounds = futil.vector.get_world_bounds
function futil.vector.get_blockpos(pos)
return v_new(m_floor(pos.x / mapblock_size), m_floor(pos.y / mapblock_size), m_floor(pos.z / mapblock_size))
end
futil.get_blockpos = futil.vector.get_blockpos
function futil.vector.get_block_min(blockpos)
return v_new(blockpos.x * mapblock_size, blockpos.y * mapblock_size, blockpos.z * mapblock_size)
end
function futil.vector.get_block_max(blockpos)
return v_new(
blockpos.x * mapblock_size + (mapblock_size - 1),
blockpos.y * mapblock_size + (mapblock_size - 1),
blockpos.z * mapblock_size + (mapblock_size - 1)
)
end
function futil.vector.get_block_bounds(blockpos)
return futil.vector.get_block_min(blockpos), futil.vector.get_block_max(blockpos)
end
futil.get_block_bounds = futil.vector.get_block_bounds
function futil.vector.get_block_center(blockpos)
return v_add(futil.vector.get_block_min(blockpos), 8) -- 8 = 16 / 2
end
function futil.vector.get_chunkpos(pos)
return v_new(
m_floor((pos.x - map_min_i) / chunksize_nodes),
m_floor((pos.y - map_min_i) / chunksize_nodes),
m_floor((pos.z - map_min_i) / chunksize_nodes)
)
end
futil.get_chunkpos = futil.vector.get_chunkpos
function futil.vector.get_chunk_bounds(chunkpos)
return v_new(
chunkpos.x * chunksize_nodes + map_min_i,
chunkpos.y * chunksize_nodes + map_min_i,
chunkpos.z * chunksize_nodes + map_min_i
),
v_new(
chunkpos.x * chunksize_nodes + map_min_i + (chunksize_nodes - 1),
chunkpos.y * chunksize_nodes + map_min_i + (chunksize_nodes - 1),
chunkpos.z * chunksize_nodes + map_min_i + (chunksize_nodes - 1)
)
end
futil.get_chunk_bounds = futil.vector.get_chunk_bounds
function futil.vector.formspec_pos(pos)
return ("%i,%i,%i"):format(pos.x, pos.y, pos.z)
end
futil.formspec_pos = futil.vector.formspec_pos
function futil.vector.iterate_area(minp, maxp)
minp, maxp = v_sort(minp, maxp)
local min_x = minp.x
local min_z = minp.z
local x = min_x - 1
local y = minp.y
local z = min_z
local max_x = maxp.x
local max_y = maxp.y
local max_z = maxp.z
return function()
if y > max_y then
return
end
x = x + 1
if x > max_x then
x = min_x
z = z + 1
end
if z > max_z then
z = min_z
y = y + 1
end
if y <= max_y then
return v_new(x, y, z)
end
end
end
futil.iterate_area = futil.vector.iterate_area
function futil.vector.iterate_volume(pos, radius)
return futil.iterate_area(futil.get_bounds(pos, radius))
end
futil.iterate_volume = futil.vector.iterate_volume
function futil.is_pos_in_bounds(minp, pos, maxp)
minp, maxp = v_sort(minp, maxp)
return (in_bounds(minp.x, pos.x, maxp.x) and in_bounds(minp.y, pos.y, maxp.y) and in_bounds(minp.z, pos.z, maxp.z))
end
function futil.vector.is_inside_world_bounds(pos)
return futil.is_pos_in_bounds(map_min_p, pos, map_max_p)
end
function futil.vector.is_blockpos_inside_world_bounds(blockpos)
return futil.vector.is_inside_world_bounds(futil.vector.get_block_min(blockpos))
end
futil.is_inside_world_bounds = futil.vector.is_inside_world_bounds
function futil.vector.bound_position_to_world(pos)
return v_new(
bound(map_min_i, pos.x, map_max_i),
bound(map_min_i, pos.y, map_max_i),
bound(map_min_i, pos.z, map_max_i)
)
end
futil.bound_position_to_world = futil.vector.bound_position_to_world
function futil.vector.volume(pos1, pos2)
local minp, maxp = v_sort(pos1, pos2)
return (maxp.x - minp.x + 1) * (maxp.y - minp.y + 1) * (maxp.z - minp.z + 1)
end
function futil.split_region_by_mapblock(pos1, pos2, num_blocks)
local chunk_size = 16 * (num_blocks or 1)
local chunk_span = chunk_size - 1
pos1, pos2 = vector.sort(pos1, pos2)
local min_x = pos1.x
local min_y = pos1.y
local min_z = pos1.z
local max_x = pos2.x
local max_y = pos2.y
local max_z = pos2.z
local x1 = min_x - (min_x % chunk_size)
local x2 = max_x - (max_x % chunk_size) + chunk_span
local y1 = min_y - (min_y % chunk_size)
local y2 = max_y - (max_y % chunk_size) + chunk_span
local z1 = min_z - (min_z % chunk_size)
local z2 = max_z - (max_z % chunk_size) + chunk_span
local chunks = {}
for y = y1, y2, chunk_size do
local y_min = m_max(min_y, y)
local y_max = m_min(max_y, y + chunk_span)
for x = x1, x2, chunk_size do
local x_min = m_max(min_x, x)
local x_max = m_min(max_x, x + chunk_span)
for z = z1, z2, chunk_size do
local z_min = m_max(min_z, z)
local z_max = m_min(max_z, z + chunk_span)
chunks[#chunks + 1] = { v_new(x_min, y_min, z_min), v_new(x_max, y_max, z_max) }
end
end
end
return chunks
end
function futil.random_unit_vector()
local u = m_random()
local v = m_random()
local lambda = m_acos(2 * u - 1) - (m_pi / 2)
local phi = 2 * m_pi * v
return v_new(m_cos(lambda) * m_cos(phi), m_cos(lambda) * m_sin(phi), m_sin(lambda))
end
---- https://math.stackexchange.com/a/205589
--function futil.random_unit_vector_in_solid_angle(theta, direction)
-- local z = m_random() * (1 - m_cos(theta)) - 1
-- local phi = m_random() * 2 * m_pi
-- local z2 = (1 - z*z) ^ 0.5
-- local ruv = v_new(z2 * m_cos(phi), z2 * m_sin(phi), z)
-- direction = direction:normalize()
-- ...
--end
function futil.is_indoors(pos, distance, trials, hits_needed)
distance = distance or 20
trials = trials or 11
hits_needed = hits_needed or 9
local num_hits = 0
for _ = 1, trials do
local ruv = futil.random_unit_vector()
local target = pos + (distance * ruv)
local hit = Raycast(pos, target, false, false)()
if hit then
num_hits = num_hits + 1
if num_hits == hits_needed then
return true
end
end
end
return false
end
function futil.can_see_sky(pos, distance, trials, max_hits)
distance = distance or 200
trials = trials or 11
max_hits = max_hits or 9
local num_hits = 0
for _ = 1, trials do
local ruv = futil.random_unit_vector()
ruv.y = m_abs(ruv.y) -- look up, not at the ground
local target = pos + (distance * ruv)
local hit = Raycast(pos, target, false, false)()
if hit then
num_hits = num_hits + 1
if num_hits > max_hits then
return false
end
end
end
return true
end
function futil.vector.is_valid_position(pos)
if type(pos) ~= "table" then
return false
elseif not (type(pos.x) == "number" and type(pos.y) == "number" and type(pos.z) == "number") then
return false
else
return futil.is_inside_world_bounds(vector.round(pos))
end
end
-- minetest.hash_node_position only works with integer coordinates
function futil.vector.hash(pos)
return string.format("%a:%a:%a", pos.x, pos.y, pos.z)
end
function futil.vector.unhash(string)
local x, y, z = string:match("^([^:]+):([^:]+):([^:]+)$")
x, y, z = tonumber(x), tonumber(y), tonumber(z)
if not (x and y and z) then
return
end
return v_new(x, y, z)
end
function futil.vector.ldistance(pos1, pos2, p)
if p == math.huge then
return m_max(m_abs(pos1.x - pos2.x), m_abs(pos1.y - pos2.y), m_abs(pos1.z - pos2.z))
else
return m_pow(
m_pow(m_abs(pos1.x - pos2.x), p) + m_pow(m_abs(pos1.y - pos2.y), p) + m_pow(m_abs(pos1.z - pos2.z), p),
1 / p
)
end
end
function futil.vector.round(pos, mult)
local round = futil.math.round
return v_new(round(pos.x, mult), round(pos.y, mult), round(pos.z, mult))
end
-- https://msl.cs.uiuc.edu/planning/node102.html
function futil.vector.rotation_to_matrix(rotation)
local cosp = m_cos(rotation.x)
local sinp = m_sin(rotation.x)
local pitch = {
{ cosp, 0, sinp },
{ 0, 1, 0 },
{ -sinp, 0, cosp },
}
local cosy = m_cos(rotation.y)
local siny = m_sin(rotation.y)
local yaw = {
{ cosy, -siny, 0 },
{ siny, cosy, 0 },
{ 0, 0, 1 },
}
local cosr = m_cos(rotation.z)
local sinr = m_sin(rotation.z)
local roll = {
{ 1, 0, 0 },
{ 0, cosr, -sinr },
{ 0, sinr, cosr },
}
return futil.matrix.multiply(futil.matrix.multiply(yaw, pitch), roll)
end
-- https://msl.cs.uiuc.edu/planning/node103.html
function futil.vector.matrix_to_rotation(matrix)
local pitch = m_atan2(matrix[2][1], matrix[1][1])
local yaw = m_asin(-matrix[3][1])
local roll = m_atan2(matrix[3][2], matrix[3][3])
return v_new(pitch, yaw, roll)
end
function futil.vector.inverse_rotation(rot)
-- since the determinant of a rotation matrix is 1, the inverse is just the transpose and i don't have to write
-- a matrix inverter
return futil.vector.matrix_to_rotation(futil.matrix.transpose(futil.vector.rotation_to_matrix(rot)))
end
-- assumed in radians
function futil.vector.compose_rotations(rot1, rot2)
local m1 = futil.vector.rotation_to_matrix(rot1)
local m2 = futil.vector.rotation_to_matrix(rot2)
return futil.vector.matrix_to_rotation(futil.matrix.multiply(m1, m2))
end
-- https://palitri.com/vault/stuff/maths/Rays%20closest%20point.pdf
-- this was originally part of the ballistics mod but i don't need it there anymore
function futil.vector.closest_point_to_two_lines(last_pos, last_vel, cur_pos, cur_vel, threshold)
threshold = threshold or 0.0001 -- if certain values are too close to 0, the results will not be good
local a = cur_vel
local b = last_vel
local a2 = a:dot(a)
if a2 < threshold then
return
end
local b2 = b:dot(b)
if b2 < threshold then
return
end
local ab = a:dot(b)
local denom = (a2 * b2) - (ab * ab)
if denom < threshold then
return
end
local A = cur_pos
local B = last_pos
local c = last_pos - cur_pos
local bc = b:dot(c)
local ac = a:dot(c)
local D = A + a * ((ac * b2 - ab * bc) / denom)
local E = B + b * ((ab * ac - bc * a2) / denom)
return (D + E) / 2
end
function futil.vector.v2f_to_float_32(v)
return {
x = futil.math.to_float32(v.x),
y = futil.math.to_float32(v.y),
}
end

12
mods/futil/mod.conf Normal file
View file

@ -0,0 +1,12 @@
name = futil
title = futil
description = flux's utility mod
website = https://content.minetest.net/packages/rheo/futil/
author = rheo
license = LGPL-3.0-or-later
media_license = CC-BY-SA-4.0
version = 2024-12-14
min_minetest_version = 5.8.0
supported_games = *
depends = fmod
release = 29065

View file

@ -0,0 +1,51 @@
futil.bisect = {}
function futil.bisect.right(t, x, low, high, key)
low = low or 1
high = high or #t + 1
if key then
while low < high do
local mid = math.floor((low + high) / 2)
if x < key(t[mid]) then
high = mid
else
low = mid + 1
end
end
else
while low < high do
local mid = math.floor((low + high) / 2)
if x < t[mid] then
high = mid
else
low = mid + 1
end
end
end
return low
end
function futil.bisect.left(t, x, low, high, key)
low = low or 1
high = high or #t + 1
if key then
while low < high do
local mid = math.floor((low + high) / 2)
if key(t[mid]) < x then
low = mid + 1
else
high = mid
end
end
else
while low < high do
local mid = math.floor((low + high) / 2)
if t[mid] < x then
low = mid + 1
else
high = mid
end
end
end
return low
end

89
mods/futil/util/class.lua Normal file
View file

@ -0,0 +1,89 @@
function futil.class1(super)
local class = {}
class.__index = class -- this becomes the index "metamethod" of objects
setmetatable(class, {
__index = super and super.__index or super,
__call = function(this_class, ...)
local obj = setmetatable({}, this_class)
local init = obj._init
if init then
init(obj, ...)
end
return obj
end,
})
function class:is_a(class2)
if class == class2 then
return true
end
if super and super:is_a(class2) then
return true
end
return false
end
return class
end
function futil.class(...)
local class = {}
class.__index = class
local meta = {
__call = function(this_class, ...)
local obj = setmetatable({}, this_class)
local init = obj._init
if init then
init(obj, ...)
end
return obj
end,
}
local parents = { ... }
class._parents = parents
if #parents > 0 then
function meta:__index(key)
for i = #parents, 1, -1 do
local parent = parents[i]
local index = parent.__index
local v
if index then
if type(index) == "function" then
v = index(self, key)
else
v = index[key]
end
else
v = parent[key]
end
if v then
return v
end
end
end
end
setmetatable(class, meta)
function class:is_a(class2)
if class == class2 then
return true
end
for _, parent in ipairs(parents) do
if parent:is_a(class2) then
return true
end
end
return false
end
return class
end

View file

@ -0,0 +1,9 @@
function futil.coalesce(...)
local arg = futil.table.pack(...)
for i = 1, arg.n do
local v = arg[i]
if v ~= nil then
return v
end
end
end

View file

@ -0,0 +1,28 @@
local table_size = futil.table.size
local function equals(a, b)
local t = type(a)
if t ~= type(b) then
return false
end
if t ~= "table" then
return a == b
elseif a == b then
return true
end
local size_a = 0
for key, value in pairs(a) do
if not equals(value, b[key]) then
return false
end
size_a = size_a + 1
end
return size_a == table_size(b)
end
futil.equals = equals

View file

@ -0,0 +1,29 @@
function futil.safe_wrap(func, rv_on_fail, error_callback)
-- wrap a function w/ logic to avoid crashing
return function(...)
local rvs = { xpcall(func, debug.traceback, ...) }
if rvs[1] then
return unpack(rvs, 2)
else
if error_callback then
error_callback(debug.getinfo(func), { ... }, rvs[2])
else
futil.log(
"error",
"(check_call): %s args: %s out: %s",
dump(debug.getinfo(func)),
dump({ ... }),
rvs[2]
)
end
return rv_on_fail
end
end
end
function futil.safe_call(func, rv_on_fail, error_callback, ...)
return futil.safe_wrap(func, rv_on_fail, error_callback)(...)
end
futil.check_call = futil.safe_wrap -- backwards compatibility

37
mods/futil/util/file.lua Normal file
View file

@ -0,0 +1,37 @@
function futil.file_exists(path)
local f = io.open(path, "r")
if f then
io.close(f)
return true
else
return false
end
end
function futil.load_file(filename)
local file = io.open(filename, "r")
if not file then
return
end
local contents = file:read("*a")
file:close()
return contents
end
-- minetest.safe_file_write is apparently unreliable on windows
function futil.write_file(filename, contents)
local file = io.open(filename, "w")
if not file then
return false
end
file:write(contents)
file:close()
return true
end

View file

@ -0,0 +1,159 @@
local functional = {}
local t_iterate = futil.table.iterate
local t_insert = table.insert
function functional.noop()
-- the NOTHING function does nothing.
end
function functional.identity(x)
return x
end
function functional.izip(...)
local is = { ... }
if #is == 0 then
return functional.noop
end
return function()
local t = {}
for i in t_iterate(is) do
local v = i()
if v ~= nil then
t_insert(t, v)
else
return
end
end
return t
end
end
function functional.zip(...)
local is = {}
for t in t_iterate({ ... }) do
t_insert(is, t_iterate(t))
end
return functional.izip(unpack(is))
end
function functional.imap(func, ...)
local zipper = functional.izip(...)
return function()
local args = zipper()
if args then
return func(unpack(args))
end
end
end
function functional.map(func, ...)
local zipper = functional.zip(...)
return function()
local args = zipper()
if args then
return func(unpack(args))
end
end
end
function functional.apply(func, t)
local t2 = {}
for k, v in pairs(t) do
t2[k] = func(v)
end
return t2
end
function functional.reduce(func, t, initial)
local i = t_iterate(t)
if not initial then
initial = i()
end
local next = i()
while next do
initial = func(initial, next)
next = i()
end
return initial
end
function functional.partial(func, ...)
local args = { ... }
return function(...)
return func(unpack(args), ...)
end
end
function functional.compose(a, b)
return function(...)
return a(b(...))
end
end
function functional.ifilter(pred, i)
local v
return function()
v = i()
while v ~= nil and not pred(v) do
v = i()
end
return v
end
end
function functional.filter(pred, t)
return functional.ifilter(pred, t_iterate(t))
end
function functional.iall(i)
while true do
local v = i()
if v == false then
return false
elseif v == nil then
return true
end
end
end
function functional.all(t)
for i = 1, #t do
if not t[i] then
return false
end
end
return true
end
function functional.iany(i)
while true do
local v = i()
if v == nil then
return false
elseif v then
return true
end
end
end
function functional.any(t)
for i = 1, #t do
if t[i] then
return true
end
end
return false
end
function functional.wrap(f)
return function(...)
return f(...)
end
end
futil.functional = functional

10
mods/futil/util/http.lua Normal file
View file

@ -0,0 +1,10 @@
local function char_to_hex(c)
return string.format("%%%02X", string.byte(c))
end
function futil.urlencode(text)
text = text:gsub("\n", "\r\n")
text = text:gsub("([^0-9a-zA-Z !'()*._~-])", char_to_hex)
text = text:gsub(" ", "+")
return text
end

24
mods/futil/util/init.lua Normal file
View file

@ -0,0 +1,24 @@
futil.dofile("util", "bisect")
futil.dofile("util", "class")
futil.dofile("util", "coalesce")
futil.dofile("util", "exception")
futil.dofile("util", "file")
futil.dofile("util", "http")
futil.dofile("util", "list")
futil.dofile("util", "math")
futil.dofile("util", "matrix")
futil.dofile("util", "memoization")
futil.dofile("util", "memory")
futil.dofile("util", "path")
futil.dofile("util", "predicates")
futil.dofile("util", "string")
futil.dofile("util", "table")
futil.dofile("util", "equals") -- depends on table
futil.dofile("util", "functional") -- depends on table
futil.dofile("util", "iterators") -- depends on functional
futil.dofile("util", "random") -- depends on math
futil.dofile("util", "regex") -- depends on exception
futil.dofile("util", "selection") -- depends on table, math
futil.dofile("util", "time") -- depends on math
futil.dofile("util", "limiters") -- depends on functional

View file

@ -0,0 +1,106 @@
local iterators = {}
function iterators.range(...)
local a, b, c = ...
if type(a) ~= "number" then
error("invalid range")
end
if not b then
a, b = 1, a
end
if type(b) ~= "number" then
error("invalid range")
end
c = c or 1
if type(c) ~= "number" or c == 0 then
error("invalid range")
end
if c > 0 then
return function()
if a > b then
return
end
local to_return = a
a = a + c
return to_return
end
else
return function()
if a < b then
return
end
local to_return = a
a = a + c
return to_return
end
end
end
function iterators.repeat_(value, times)
if times then
local i = 0
return function()
i = i + 1
if i <= times then
return value
end
end
else
return function()
return value
end
end
end
function iterators.chain(...)
local arg = { ... }
local i = 1
return function()
while i <= #arg do
local v = arg[i]()
if v then
return v
end
end
end
end
function iterators.count(start, step)
step = step or 1
return function()
local rv = start
start = start + step
return rv
end
end
function iterators.values(t)
local k
return function()
local value
k, value = next(t, k)
return value
end
end
function iterators.accumulate(t, composer, initial)
local value = initial
local i = futil.table.iterate(t)
return function()
local next_value = i()
if next_value then
if value == nil then
value = next_value
elseif composer then
value = composer(value, next_value)
else
value = value + next_value
end
return value
end
end
end
futil.iterators = iterators

View file

@ -0,0 +1,42 @@
--[[
functions to limit the growth of a variable.
the intention here is to provide a family of functions for which
* f(x) is defined for all x >= 0
* f(0) = 0
* f(1) = 1
* f is continuous
* f is nondecreasing
* f\'(x) is nonincreasing when x > 1 (when the parameters are appropriate)
]]
local log = math.log
local pow = math.pow
local tanh = math.tanh
futil.limiters = {
-- no limiting
none = futil.functional.identity,
-- f(x) = x ^ param_1. param_1 should be < 1 for f\'(x) to be nonincreasing
-- f(x) will grow arbitrarily, but at a decreasing rate.
gamma = function(x, param_1)
return pow(x, param_1)
end,
-- the hyperbolic tangent scaled so that f(0) = 0 and f(1) = 1.
-- f(x) will grow approximately linearly for small x, but it will never grow beyond a maximum value, which is
-- approximately equal to param_1 + 1
tanh = function(x, param_1)
return (tanh((x - 1) / param_1) - tanh(-1 / param_1)) / -tanh(-1 / param_1)
end,
-- f(x) = log^param_2(param_1 * x + 1), scaled so that f(0) = 0 and f(1) = 1.
-- f(x) will grow arbitrarily, but at a much slower rate than a gamma limiter
log__n = function(x, param_1, param_2)
return (log(x + 1) * pow(log(param_1 * x + 1), param_2) / (log(2) * pow(log(param_1 + 1), param_2)))
end,
}
function futil.create_limiter(name, param_1, param_2)
local f = futil.limiters[name]
return function(x)
return f(x, param_1, param_2)
end
end

19
mods/futil/util/list.lua Normal file
View file

@ -0,0 +1,19 @@
function futil.list(iterator)
local t = {}
local v = iterator()
while v do
t[#t + 1] = v
v = iterator()
end
return t
end
function futil.list_multiple(iterator)
local t = {}
local v = { iterator() }
while #v > 0 do
t[#t + 1] = v
v = { iterator() }
end
return t
end

162
mods/futil/util/math.lua Normal file
View file

@ -0,0 +1,162 @@
futil.math = {}
local floor = math.floor
local huge = math.huge
local max = math.max
local min = math.min
function futil.math.idiv(a, b)
local rem = a % b
return (a - rem) / b, rem
end
function futil.math.bound(m, v, M)
return max(m, min(v, M))
end
function futil.math.in_bounds(m, v, M)
return m <= v and v <= M
end
local in_bounds = futil.math.in_bounds
function futil.math.is_integer(v)
return floor(v) == v
end
local is_integer = futil.math.is_integer
function futil.math.is_u8(i)
return (type(i) == "number" and is_integer(i) and in_bounds(0, i, 0xFF))
end
function futil.math.is_u16(i)
return (type(i) == "number" and is_integer(i) and in_bounds(0, i, 0xFFFF))
end
function futil.math.sum(t, initial)
local sum
local start
if initial then
sum = initial
start = 1
else
sum = t[1]
start = 2
end
for i = start, #t do
sum = sum + t[i]
end
return sum
end
function futil.math.isum(i, initial)
local sum
if initial == nil then
sum = i()
else
sum = initial
end
local v = i()
while v do
sum = sum + v
v = i()
end
return sum
end
function futil.math.product(t, initial)
local product
local start
if initial then
product = initial
start = 1
else
product = t[1]
start = 2
end
for i = start, #t do
product = product * t[i]
end
return product
end
function futil.math.iproduct(i, initial)
local product
if initial == nil then
product = i()
else
product = initial
end
local v = i()
while v do
product = product * v
v = i()
end
return product
end
function futil.math.probabilistic_round(v)
return floor(v + math.random())
end
function futil.math.cmp(a, b)
return a < b
end
futil.math.deg2rad = math.deg
futil.math.rad2deg = math.rad
function futil.math.do_intervals_overlap(min1, max1, min2, max2)
return min1 <= max2 and min2 <= max1
end
-- i took one class from kahan and can't stop doing this
local function round(n)
local d = n % 1
local i = n - d
if i % 2 == 0 then
if d <= 0.5 then
return i
else
return i + 1
end
else
if d < 0.5 then
return i
else
return i + 1
end
end
end
function futil.math.round(number, mult)
if mult then
return round(number / mult) * mult
else
return round(number)
end
end
-- TODO this doesn't handle out-of-bounds exponents
function futil.math.to_float32(number)
if number == huge or number == -huge or number ~= number then
return number
end
local sign, significand, exponent = ("%a"):format(number):match("^(-?)0x([0-9a-f\\.]+)p([0-9+-]+)$")
return tonumber(("%s0x%sp%s"):format(sign, significand:sub(1, 8), exponent))
end

View file

@ -0,0 +1,30 @@
futil.matrix = {}
function futil.matrix.multiply(m1, m2)
assert(#m1[1] == #m2, "width of first argument must be height of second")
local product = {}
for i = 1, #m1 do
local row = {}
for j = 1, #m2[1] do
local value = 0
for k = 1, #m2 do
value = value + m1[i][k] * m2[k][j]
end
row[j] = value
end
product[i] = row
end
return product
end
function futil.matrix.transpose(m)
local t = {}
for i = 1, #m[1] do
local row = {}
for j = 1, #m do
row[j] = m[j][i]
end
t[i] = row
end
return t
end

View file

@ -0,0 +1,51 @@
local private_state = ...
local mod_storage = private_state.mod_storage
function futil.memoize1(func)
local memo = {}
return function(arg)
if arg == nil then
return func(arg)
end
local rv = memo[arg]
if not rv then
rv = func(arg)
memo[arg] = rv
end
return rv
end
end
function futil.memoize_dumpable(func)
local memo = {}
return function(...)
local key = dump({ ... })
local rv = memo[key]
if not rv then
rv = func(...)
memo[key] = rv
end
return rv
end
end
function futil.memoize1_modstorage(id, func)
local key_format = ("%%s:%s:memoize"):format(id)
return function(arg)
local key_key = key_format:format(tostring(arg))
local rv = mod_storage:get(key_key)
if not rv then
rv = func(arg)
mod_storage:set_string(key_key, tostring(rv))
end
return rv
end
end
futil.memoize1ms = futil.memoize1_modstorage -- backwards compatibility

View file

@ -0,0 +1,45 @@
-- i have no idea how accurate this is, i use documentation from the below link for a few things
-- https://wowwiki-archive.fandom.com/wiki/Lua_object_memory_sizes
local function estimate_memory_usage(thing, seen)
local typ = type(thing)
if typ == "nil" then
return 0
end
seen = seen or {}
if seen[thing] then
return 0
end
seen[thing] = true
if typ == "boolean" then
return 4
elseif typ == "number" then
return 8 -- this is probably larger?
elseif typ == "string" then
return 25 + typ:len()
elseif typ == "function" then
-- TODO: we can calculate the usage of upvalues, but that's complicated
return 40
elseif typ == "userdata" then
return 0 -- this is probably larger
elseif typ == "thread" then
return 1224 -- this is probably larger
elseif typ == "table" then
local size = 64
for k, v in pairs(thing) do
if type(k) == "number" then
size = size + 16 + estimate_memory_usage(v, seen)
else
size = size + 40 + estimate_memory_usage(k, seen) + estimate_memory_usage(v, seen)
end
end
return size
else
futil.log("warning", "estimate_memory_usage: unknown type %s", typ)
return 0 -- ????
end
end
futil.estimate_memory_usage = estimate_memory_usage

7
mods/futil/util/path.lua Normal file
View file

@ -0,0 +1,7 @@
function futil.path_concat(...)
return table.concat({ ... }, DIR_DELIM)
end
function futil.path_split(path)
return string.split(path, DIR_DELIM, true)
end

View file

@ -0,0 +1,43 @@
function futil.is_nil(v)
return v == nil
end
function futil.is_boolean(v)
return v == true or v == false
end
function futil.is_number(v)
return type(v) == "number"
end
function futil.is_positive(v)
return v > 0
end
function futil.is_integer(v)
return v % 1 == 0
end
function futil.is_positive_integer(v)
return type(v) == "number" and v > 0 and v % 1 == 0
end
function futil.is_string(v)
return type(v) == "string"
end
function futil.is_userdata(v)
return type(v) == "userdata"
end
function futil.is_function(v)
return type(v) == "function"
end
function futil.is_thread(v)
return type(v) == "thread"
end
function futil.is_table(v)
return type(v) == "table"
end

View file

@ -0,0 +1,84 @@
local f = string.format
futil.random = {}
function futil.random.choice(t, random)
random = random or math.random
return t[random(#t)]
end
function futil.random.weighted_choice(t, random)
random = random or math.random
local elements, weights = {}, {}
local i = 1
for element, weight in pairs(t) do
elements[i] = element
weights[i] = weight
i = i + 1
end
local breaks = futil.list(futil.iterators.accumulate(weights))
local value = random() * breaks[#breaks]
return elements[futil.bisect.right(breaks, value)]
end
local WeightedChooser = futil.class1()
function WeightedChooser:_init(t)
local elements, weights = {}, {}
local i = 1
for element, weight in pairs(t) do
elements[i] = element
weights[i] = weight
i = i + 1
end
self._elements = elements
self._breaks = futil.list(futil.iterators.accumulate(weights))
end
function WeightedChooser:next(random)
random = random or math.random
local breaks = self._breaks
local value = random() * breaks[#breaks]
return self._elements[futil.bisect.right(breaks, value)]
end
futil.random.WeightedChooser = WeightedChooser
function futil.random.choice(t, random)
assert(#t > 0, "cannot get choice from an empty table")
random = random or math.random
return t[random(#t)]
end
-- https://stats.stackexchange.com/questions/569647/
function futil.random.sample(t, k, random)
assert(k <= #t, f("cannot sample %i items from a set of size %i", k, #t))
random = random or math.random
local sample = {}
for i = 1, k do
sample[i] = t[i]
end
for j = k + 1, #t do
if random() < k / j then
sample[random(1, k)] = t[j]
end
end
return sample
end
function futil.random.sample_with_indices(t, k, random)
assert(k <= #t, f("cannot sample %i items from a set of size %i", k, #t))
random = random or math.random
local sample = {}
for i = 1, k do
sample[i] = { i, t[i] }
end
for j = k + 1, #t do
if random() < k / j then
sample[random(1, k)] = { j, t[j] }
end
end
return sample
end

View file

@ -0,0 +1,6 @@
function futil.is_valid_regex(pattern)
return futil.safe_call(function()
(""):match(pattern)
return true
end, false, futil.functional.noop)
end

View file

@ -0,0 +1,109 @@
local floor = math.floor
local min = math.min
local random = math.random
local swap = futil.table.swap
local default_cmp = futil.math.cmp
futil.selection = {}
local function partition5(t, left, right, cmp)
cmp = cmp or default_cmp
local i = left + 1
while i <= right do
local j = i
while j > left and cmp(t[j], t[j - 1]) do
swap(t, j - 1, j)
j = j - 1
end
i = i + 1
end
return floor((left + right) / 2)
end
local function partition(t, left, right, pivot_i, i, cmp)
cmp = cmp or default_cmp
local pivot_v = t[pivot_i]
swap(t, pivot_i, right)
local store_i = left
for j = left, right - 1 do
if cmp(t[j], pivot_v) then
swap(t, store_i, j)
store_i = store_i + 1
end
end
local store_i_eq = store_i
for j = store_i, right - 1 do
if t[j] == pivot_v then
swap(t, store_i_eq, j)
store_i_eq = store_i_eq + 1
end
end
swap(t, right, store_i_eq)
if i < store_i then
return store_i
elseif i <= store_i_eq then
return i
else
return store_i_eq
end
end
local function quickselect(t, left, right, i, pivot_alg, cmp)
cmp = cmp or default_cmp
while true do
if left == right then
return left
end
local pivot_i = partition(t, left, right, pivot_alg(t, left, right, cmp), i, cmp)
if i == pivot_i then
return i
elseif i < pivot_i then
right = pivot_i - 1
else
left = pivot_i + 1
end
end
end
futil.selection.quickselect = quickselect
futil.selection.pivot = {}
function futil.selection.pivot.random(t, left, right, cmp)
return random(left, right)
end
local function pivot_medians_of_medians(t, left, right, cmp)
cmp = cmp or default_cmp
if right - left < 5 then
return partition5(t, left, right, cmp)
end
for i = left, right, 5 do
local sub_right = min(i + 4, right)
local median5 = partition5(t, i, sub_right, cmp)
swap(t, median5, left + floor((i - left) / 5))
end
local mid = floor((right - left) / 10) + left + 1
return quickselect(t, left, left + floor((right - left) / 5), mid, pivot_medians_of_medians, cmp)
end
futil.selection.pivot.median_of_medians = pivot_medians_of_medians
--[[
make use of quickselect to munge a table:
median_index = math.floor(#t / 2)
after calling this,
t[1] through t[median_index - 1] will be the elements less than t[median_index]
t[median_index] will be the median (or element less-than-the-median for even length tables)
t[median_index + 1] through t[#t] will be the elements greater than t[median_index]
pivot is a pivot algorithm, defaults to random selection
cmp is a comparison function.
returns median_index.
]]
function futil.selection.select(t, pivot_alg, cmp)
cmp = cmp or default_cmp
pivot_alg = pivot_alg or futil.selection.pivot.random
local median_index = math.floor(#t / 2)
return quickselect(t, 1, #t, median_index, pivot_alg, cmp)
end

View file

@ -0,0 +1,62 @@
futil.string = {}
function futil.string.truncate(s, max_length, suffix)
suffix = suffix or "..."
if s:len() > max_length then
return s:sub(1, max_length - suffix:len()) .. suffix
else
return s
end
end
function futil.string.lc_cmp(a, b)
return a:lower() < b:lower()
end
function futil.string.startswith(s, start, start_i, end_i)
return s:sub(start_i or 0, end_i or #s):sub(1, #start) == start
end
local escape_pattern = "([%(%)%.%%%+%-%*%?%[%^%$])"
local function escape_regex(str)
return str:gsub(escape_pattern, "%%%1")
end
local glob_patterns = {
["?"] = ".",
["*"] = ".*",
}
local function transform_pattern(pattern)
local parts = {}
local start = 1
for i = 1, #pattern do
local glob_pattern = glob_patterns[pattern:sub(i)]
if glob_pattern then
if start < i then
parts[#parts + 1] = escape_regex(pattern:sub(start, i - 1))
end
parts[#parts + 1] = glob_pattern
start = i + 1
end
end
if start < #pattern then
parts[#parts + 1] = escape_regex(pattern:sub(start, #pattern))
end
return table.concat(parts, "")
end
function futil.string.globmatch(str, pattern)
return str:match(transform_pattern(pattern))
end
futil.GlobMatcher = futil.class1()
function futil.GlobMatcher:_init(pattern)
self._pattern = transform_pattern(pattern)
end
function futil.GlobMatcher:match(str)
return str:match(self._pattern)
end

188
mods/futil/util/table.lua Normal file
View file

@ -0,0 +1,188 @@
local default_cmp = futil.math.cmp
futil.table = {}
function futil.table.set_all(t1, t2)
for k, v in pairs(t2) do
t1[k] = v
end
return t1
end
function futil.table.compose(t1, t2)
local t = table.copy(t1)
futil.table.set_all(t, t2)
return t
end
function futil.table.pairs_by_value(t, cmp)
cmp = cmp or default_cmp
local s = {}
for k, v in pairs(t) do
table.insert(s, { k, v })
end
table.sort(s, function(a, b)
return cmp(a[2], b[2])
end)
local i = 0
return function()
i = i + 1
local v = s[i]
if v then
return unpack(v)
else
return nil
end
end
end
function futil.table.pairs_by_key(t, cmp)
cmp = cmp or default_cmp
local s = {}
for k, v in pairs(t) do
table.insert(s, { k, v })
end
table.sort(s, function(a, b)
return cmp(a[1], b[1])
end)
local i = 0
return function()
i = i + 1
local v = s[i]
if v then
return unpack(v)
else
return nil
end
end
end
function futil.table.size(t)
local size = 0
for _ in pairs(t) do
size = size + 1
end
return size
end
function futil.table.is_empty(t)
return next(t) == nil
end
function futil.table.count_elements(t)
local counts = {}
for _, item in ipairs(t) do
counts[item] = (counts[item] or 0) + 1
end
return counts
end
function futil.table.sets_intersect(set1, set2)
for k in pairs(set1) do
if set2[k] then
return true
end
end
return false
end
function futil.table.iterate(t)
local i = 0
return function()
i = i + 1
return t[i]
end
end
function futil.table.reversed(t)
local len = #t
local reversed = {}
for i = len, 1, -1 do
reversed[len - i + 1] = t[i]
end
return reversed
end
function futil.table.contains(t, value)
for _, v in ipairs(t) do
if v == value then
return true
end
end
return false
end
function futil.table.keys(t)
local keys = {}
for key in pairs(t) do
keys[#keys + 1] = key
end
return keys
end
function futil.table.ikeys(t)
local key
return function()
key = next(t, key)
return key
end
end
function futil.table.values(t)
local values = {}
for _, value in pairs(t) do
values[#values + 1] = value
end
return values
end
function futil.table.sort_keys(t, cmp)
local keys = futil.table.keys(t)
table.sort(keys, cmp)
return keys
end
-- https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
function futil.table.shuffle(t, rnd)
rnd = rnd or math.random
for i = #t, 2, -1 do
local j = rnd(i)
t[i], t[j] = t[j], t[i]
end
return t
end
local function swap(t, i, j)
t[i], t[j] = t[j], t[i]
end
futil.table.swap = swap
function futil.table.get(t, key, default)
local value = t[key]
if value == nil then
return default
end
return value
end
function futil.table.setdefault(t, key, default)
local value = t[key]
if value == nil then
t[key] = default
return default
end
return value
end
function futil.table.pack(...)
return { n = select("#", ...), ... }
end

29
mods/futil/util/time.lua Normal file
View file

@ -0,0 +1,29 @@
local idiv = futil.math.idiv
-- convert a number of seconds into a more human-readable value
-- ignores the actual passage of time and assumes all years are 365 days
function futil.seconds_to_interval(time)
local s, m, h, d
time, s = idiv(time, 60)
time, m = idiv(time, 60)
time, h = idiv(time, 24)
time, d = idiv(time, 365)
if time ~= 0 then
return ("%d years %d days %02d:%02d:%02d"):format(time, d, h, m, s)
elseif d ~= 0 then
return ("%d days %02d:%02d:%02d"):format(d, h, m, s)
elseif h ~= 0 then
return ("%02d:%02d:%02d"):format(h, m, s)
elseif m ~= 0 then
return ("%02d:%02d"):format(m, s)
else
return ("%ds"):format(s)
end
end
-- ISO 8601 date format
function futil.format_utc(timestamp)
return os.date("!%Y-%m-%dT%TZ", timestamp)
end