Charakterbewegungen hinzugefügt, Deko hinzugefügt, Kochrezepte angepasst
This commit is contained in:
parent
95945c0306
commit
a0c893ca0b
1124 changed files with 64294 additions and 763 deletions
11
mods/futil/.cdb.json
Normal file
11
mods/futil/.cdb.json
Normal 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"
|
||||
}
|
3
mods/futil/.check_date.sh
Normal file
3
mods/futil/.check_date.sh
Normal file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
grep $(date -u -I) mod.conf
|
||||
exit $?
|
14
mods/futil/.editorconfig
Normal file
14
mods/futil/.editorconfig
Normal 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
1
mods/futil/.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
github: fluxionary
|
34
mods/futil/.github/workflows/pre-commit.yml
vendored
Normal file
34
mods/futil/.github/workflows/pre-commit.yml
vendored
Normal 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
1
mods/futil/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.unfinished.*
|
658
mods/futil/.luacheckrc
Normal file
658
mods/futil/.luacheckrc
Normal 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",
|
||||
},
|
||||
}
|
41
mods/futil/.pre-commit-config.yaml
Normal file
41
mods/futil/.pre-commit-config.yaml
Normal 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
469
mods/futil/API.md
Normal 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
168
mods/futil/LICENSE.txt
Normal 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.
|
427
mods/futil/MEDIA_LICENSE.txt
Normal file
427
mods/futil/MEDIA_LICENSE.txt
Normal 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
5
mods/futil/README.md
Normal 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.
|
125
mods/futil/data_structures/bitarray.lua
Normal file
125
mods/futil/data_structures/bitarray.lua
Normal 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
|
11
mods/futil/data_structures/default_table.lua
Normal file
11
mods/futil/data_structures/default_table.lua
Normal 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
|
88
mods/futil/data_structures/deque.lua
Normal file
88
mods/futil/data_structures/deque.lua
Normal 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
|
7
mods/futil/data_structures/init.lua
Normal file
7
mods/futil/data_structures/init.lua
Normal 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
|
156
mods/futil/data_structures/pairing_heap.lua
Normal file
156
mods/futil/data_structures/pairing_heap.lua
Normal 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
|
268
mods/futil/data_structures/point_search_tree.lua
Normal file
268
mods/futil/data_structures/point_search_tree.lua
Normal 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
|
267
mods/futil/data_structures/set.lua
Normal file
267
mods/futil/data_structures/set.lua
Normal 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
|
32
mods/futil/data_structures/sparse_graph.lua
Normal file
32
mods/futil/data_structures/sparse_graph.lua
Normal 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
11
mods/futil/init.lua
Normal 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
186
mods/futil/minetest/box.lua
Normal 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
|
31
mods/futil/minetest/dedupe.lua
Normal file
31
mods/futil/minetest/dedupe.lua
Normal 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
|
111
mods/futil/minetest/dump.lua
Normal file
111
mods/futil/minetest/dump.lua
Normal 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
|
271
mods/futil/minetest/fake_inventory.lua
Normal file
271
mods/futil/minetest/fake_inventory.lua
Normal 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
|
88
mods/futil/minetest/globalstep.lua
Normal file
88
mods/futil/minetest/globalstep.lua
Normal 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
|
61
mods/futil/minetest/group.lua
Normal file
61
mods/futil/minetest/group.lua
Normal 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
|
100
mods/futil/minetest/hud_ephemeral.lua
Normal file
100
mods/futil/minetest/hud_ephemeral.lua
Normal 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
|
172
mods/futil/minetest/hud_manager.lua
Normal file
172
mods/futil/minetest/hud_manager.lua
Normal 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)
|
171
mods/futil/minetest/image.lua
Normal file
171
mods/futil/minetest/image.lua
Normal 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
|
24
mods/futil/minetest/init.lua
Normal file
24
mods/futil/minetest/init.lua
Normal 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
|
40
mods/futil/minetest/inventory.lua
Normal file
40
mods/futil/minetest/inventory.lua
Normal 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
|
133
mods/futil/minetest/item.lua
Normal file
133
mods/futil/minetest/item.lua
Normal 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
|
200
mods/futil/minetest/object.lua
Normal file
200
mods/futil/minetest/object.lua
Normal 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
|
158
mods/futil/minetest/object_properties.lua
Normal file
158
mods/futil/minetest/object_properties.lua
Normal 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
|
30
mods/futil/minetest/raycast.lua
Normal file
30
mods/futil/minetest/raycast.lua
Normal 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
|
15
mods/futil/minetest/registration.lua
Normal file
15
mods/futil/minetest/registration.lua
Normal 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
|
87
mods/futil/minetest/serialization.lua
Normal file
87
mods/futil/minetest/serialization.lua
Normal 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
|
7
mods/futil/minetest/set_look_dir.lua
Normal file
7
mods/futil/minetest/set_look_dir.lua
Normal 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
|
205
mods/futil/minetest/strip_translation.lua
Normal file
205
mods/futil/minetest/strip_translation.lua
Normal 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
|
9
mods/futil/minetest/texture.lua
Normal file
9
mods/futil/minetest/texture.lua
Normal 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
|
7
mods/futil/minetest/time.lua
Normal file
7
mods/futil/minetest/time.lua
Normal 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
|
402
mods/futil/minetest/vector.lua
Normal file
402
mods/futil/minetest/vector.lua
Normal 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
12
mods/futil/mod.conf
Normal 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
|
51
mods/futil/util/bisect.lua
Normal file
51
mods/futil/util/bisect.lua
Normal 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
89
mods/futil/util/class.lua
Normal 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
|
9
mods/futil/util/coalesce.lua
Normal file
9
mods/futil/util/coalesce.lua
Normal 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
|
28
mods/futil/util/equals.lua
Normal file
28
mods/futil/util/equals.lua
Normal 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
|
29
mods/futil/util/exception.lua
Normal file
29
mods/futil/util/exception.lua
Normal 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
37
mods/futil/util/file.lua
Normal 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
|
159
mods/futil/util/functional.lua
Normal file
159
mods/futil/util/functional.lua
Normal 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
10
mods/futil/util/http.lua
Normal 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
24
mods/futil/util/init.lua
Normal 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
|
106
mods/futil/util/iterators.lua
Normal file
106
mods/futil/util/iterators.lua
Normal 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
|
42
mods/futil/util/limiters.lua
Normal file
42
mods/futil/util/limiters.lua
Normal 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
19
mods/futil/util/list.lua
Normal 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
162
mods/futil/util/math.lua
Normal 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
|
30
mods/futil/util/matrix.lua
Normal file
30
mods/futil/util/matrix.lua
Normal 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
|
51
mods/futil/util/memoization.lua
Normal file
51
mods/futil/util/memoization.lua
Normal 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
|
45
mods/futil/util/memory.lua
Normal file
45
mods/futil/util/memory.lua
Normal 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
7
mods/futil/util/path.lua
Normal 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
|
43
mods/futil/util/predicates.lua
Normal file
43
mods/futil/util/predicates.lua
Normal 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
|
84
mods/futil/util/random.lua
Normal file
84
mods/futil/util/random.lua
Normal 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
|
6
mods/futil/util/regex.lua
Normal file
6
mods/futil/util/regex.lua
Normal 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
|
109
mods/futil/util/selection.lua
Normal file
109
mods/futil/util/selection.lua
Normal 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
|
62
mods/futil/util/string.lua
Normal file
62
mods/futil/util/string.lua
Normal 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
188
mods/futil/util/table.lua
Normal 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
29
mods/futil/util/time.lua
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue