2023-03-31 11:11:07 -07:00

483 lines
13 KiB
Lua

local f = string.format
local S = balanced_diet.S
local s = balanced_diet.settings
balanced_diet.registered_nutrients = {}
local function gamma(v)
return math.max(0, math.min(math.pow(v, s.nutrient_decay_gamma), 1))
end
local eaten_key = "balanced_diet:eaten"
local last_set_key = "balanced_diet:last_set"
local function get_eaten(meta, now)
local eaten = minetest.deserialize(meta:get_string(eaten_key))
if now and eaten then
local last_set = meta:get_int(last_set_key)
if last_set > 0 and now > last_set then
local elapsed = now - last_set
for food, remaining in pairs(eaten) do
if elapsed >= remaining or not balanced_diet.is_food(food) then
eaten[food] = nil
else
eaten[food] = remaining - elapsed
end
end
meta:set_string(eaten_key, minetest.serialize(eaten), now)
end
meta:set_int(last_set_key, now)
end
return eaten or {}
end
function balanced_diet.get_eaten(player, now)
return get_eaten(player:get_meta(), now)
end
local function set_eaten(meta, eaten, now)
if futil.table.is_empty(eaten) then
meta:set_string(eaten_key, "")
else
meta:set_string(eaten_key, minetest.serialize(eaten))
end
if now then
meta:set_int(last_set_key, now)
end
end
minetest.register_on_joinplayer(function(player)
local meta = player:get_meta()
-- start the timer again
meta:set_int(last_set_key, os.time())
end)
minetest.register_on_leaveplayer(function(player, timed_out)
local meta = player:get_meta()
if timed_out then
-- refund time during timeout (60 seconds)
-- note that this doesn't refund foods which might have expired during the timeout, which is tricky
local eaten = get_eaten(meta)
for food, remaining in pairs(eaten) do
eaten[food] = remaining + 60
end
set_eaten(meta, eaten) -- don't change last_set
end
-- make sure food is used up
get_eaten(meta, os.time())
end)
function balanced_diet.register_nutrient(name, def)
if balanced_diet.registered_nutrients[name] then
error("attempt to re-register nutrient " .. name)
end
def.name = name
def.description = def.description or def.name
balanced_diet.registered_nutrients[name] = def
end
function balanced_diet.override_nutrient(name, def)
if not balanced_diet.registered_nutrients[name] then
error("attempt to override non-existing nutrient " .. name)
end
if def.name then
error("you can't rename a nutrient")
end
futil.table.set_all(balanced_diet.registered_nutrients[name], def)
end
function balanced_diet.is_food(item_or_stack)
local itemstack = ItemStack(item_or_stack)
local def = itemstack:get_definition()
return def and def._balanced_diet
end
function balanced_diet.get_food_def(item_or_stack)
local stack
if type(item_or_stack) == "string" then
stack = ItemStack(item_or_stack)
else
stack = item_or_stack
end
local meta = stack:get_meta()
local override = meta:get("_balanced_diet")
if override then
return minetest.deserialize(override)
end
return stack:get_definition()._balanced_diet
end
local function build_description(item_name, food_def)
local def = minetest.registered_items[item_name]
local orig_description
if def._balanced_diet_orig_description then
orig_description = def._balanced_diet_orig_description
else
local item_stack = ItemStack(item_name)
orig_description = item_stack:get_description()
minetest.override_item(item_name, {
_balanced_diet_orig_description = orig_description,
})
end
local parts = { orig_description }
table.insert_all(parts, {
S("food saturation: @1", food_def.saturation),
S("food duration: @1s", food_def.duration),
})
for nutrient, value in futil.table.pairs_by_key(food_def.nutrients or {}) do
table.insert(parts, S("@1 = @2", balanced_diet.registered_nutrients[nutrient].description, value))
end
return table.concat(parts, "\n"), def.short_description or orig_description
end
function balanced_diet.register_food(item_name, food_def)
local def = minetest.registered_items[item_name]
if not def then
error("attempt to register non-existent item as a food " .. item_name)
end
if def._balanced_diet then
error("attempt to re-register food " .. item_name)
end
food_def = table.copy(food_def)
food_def.duration = food_def.duration or s.default_food_duration
food_def.saturation = food_def.saturation or s.default_food_saturation
local groups = table.copy(def.groups or {})
groups.food = 1
for nutrient, value in pairs(food_def.nutrients or {}) do
if not balanced_diet.registered_nutrients[nutrient] then
-- TODO: this should optionally just be a warning
error(f("unknown nutrient %q when defining food %q", nutrient, item_name))
end
groups["nutrient_" .. nutrient] = value
end
local description, short_description = build_description(item_name, food_def)
minetest.override_item(item_name, {
_balanced_diet = food_def,
description = description,
short_description = short_description,
groups = groups,
on_use = balanced_diet.item_eat(),
})
end
function balanced_diet.override_food(item_name, overrides)
local def = minetest.registered_items[item_name]
if not def then
error("attempt to override non-existent item " .. item_name)
end
if not def._balanced_diet then
error("attempt to override unregistered food " .. item_name)
end
local groups = table.copy(def.groups)
-- clear old nutrient groups
for group in pairs(groups) do
if group:match("^nutrient_") then
groups[group] = nil
end
end
for nutrient, value in pairs(overrides.nutrients or {}) do
if not balanced_diet.registered_nutrients[nutrient] then
-- TODO: this should optionally just be a warning
error(f("unknown nutrient %q when defining food %q", nutrient, item_name))
end
groups["nutrient_" .. nutrient] = value
end
local food_def = table.copy(def._balanced_diet)
futil.table.set_all(food_def, overrides)
local description, short_description = build_description(item_name, food_def)
minetest.override_item(item_name, {
_balanced_diet = food_def,
description = description,
short_description = short_description,
groups = groups,
})
end
balanced_diet.registered_on_item_eats = {}
function balanced_diet.register_on_item_eat(callback)
table.insert(balanced_diet.registered_on_item_eats, callback)
end
balanced_diet.registered_after_item_eats = {}
function balanced_diet.register_after_item_eat(callback)
table.insert(balanced_diet.registered_after_item_eats, callback)
end
function balanced_diet.check_nutrient_value(player, nutrient, now)
if not minetest.is_player(player) then
return
end
local nutrient_def = balanced_diet.registered_nutrients[nutrient]
if not nutrient_def then
error(f("unknown nutrient %q", nutrient))
end
if not now then
now = os.time()
end
local meta = player:get_meta()
local eaten = get_eaten(meta, now)
local value = 0
for food, remaining in pairs(eaten) do
local food_def = balanced_diet.get_food_def(food)
local full_value = food_def.nutrients[nutrient] or 0
local remaining_value = full_value * gamma(remaining / food_def.duration)
value = value + remaining_value
end
return value
end
balanced_diet.registered_on_saturation_max_changes = {}
function balanced_diet.register_on_saturation_max_change(callback)
table.insert(balanced_diet.registered_on_saturation_max_changes, callback)
end
function balanced_diet.get_saturation_max(player)
if not minetest.is_player(player) then
return
end
local meta = player:get_meta()
return meta:get_float("balanced_diet:saturation_max")
end
function balanced_diet.set_saturation_max(player, saturation_max)
if not minetest.is_player(player) then
return
end
local meta = player:get_meta()
local key = "balanced_diet:saturation_max"
local old_max = meta:get_float("balanced_diet:saturation_max")
meta:set_float(key, saturation_max)
if saturation_max ~= old_max then
for _, callback in ipairs(balanced_diet.registered_on_saturation_max_changes) do
callback(player, saturation_max, old_max)
end
end
end
local current_saturation_cache = {}
minetest.register_globalstep(function()
current_saturation_cache = {}
end)
function balanced_diet.get_current_saturation(player, now)
if not minetest.is_player(player) then
return 0
end
if not now then
now = os.time()
end
local player_name = player:get_player_name()
local key = f("%s:%s", player_name, now)
local total_saturation = current_saturation_cache[key]
if total_saturation then
return total_saturation
end
local meta = player:get_meta()
local eaten = get_eaten(meta, now)
total_saturation = 0
for food, remaining in pairs(eaten) do
local food_def = balanced_diet.get_food_def(food)
if not food_def then
error(f("no def for food %s?!", food))
end
local remaining_saturation = food_def.saturation * remaining / food_def.duration
total_saturation = total_saturation + remaining_saturation
end
local saturation_max = balanced_diet.get_saturation_max(player)
if total_saturation > saturation_max then
balanced_diet.log("error", "saturation %s is greater than max %s", total_saturation, saturation_max)
total_saturation = saturation_max
end
current_saturation_cache[key] = total_saturation
return total_saturation
end
function balanced_diet.purge_eaten(player)
if not minetest.is_player(player) then
return
end
local meta = player:get_meta()
set_eaten(meta, {}, os.time())
end
balanced_diet.registered_appetite_checks = {}
function balanced_diet.register_appetite_check(callback)
table.insert(balanced_diet.registered_appetite_checks, callback)
end
function balanced_diet.check_appetite_for(player, itemstack, now)
if not minetest.is_player(player) then
return false, S("you are not a player")
end
local def = itemstack:get_definition()
if not def then
return false, S("this is not food")
end
local food_def = balanced_diet.get_food_def(itemstack)
if not food_def then
return false, S("this is not food")
end
if not now then
now = os.time()
end
for i = 1, #balanced_diet.registered_appetite_checks do
local result, reason = balanced_diet.registered_appetite_checks[i](player, itemstack, now)
if result == false then
return false, (reason or S("appetite check failed w/out reason."))
end
end
local meta = player:get_meta()
local food_name = itemstack:get_name()
local food_description = futil.get_safe_short_description(itemstack)
local food_saturation = food_def.saturation
local saturation_max = balanced_diet.get_saturation_max(player)
local saturation_after_eating = 0
local eaten = get_eaten(meta, now)
for eaten_food, remaining in pairs(eaten) do
if eaten_food == food_name then
if remaining > food_def.duration * s.top_up_at then
return false, S("you can't eat any more @1 right now.", food_description)
else
saturation_after_eating = saturation_after_eating + food_saturation
end
else
local other_food_def = balanced_diet.get_food_def(eaten_food)
local remaining_saturation = other_food_def.saturation * remaining / other_food_def.duration
saturation_after_eating = saturation_after_eating + remaining_saturation
end
end
if saturation_after_eating > saturation_max then
return false, S("you are too full to eat @1 right now.", food_description)
end
return true, nil, eaten
end
local function handle_replace_with(eater, itemstack, replace_with)
local inv = eater:get_inventory()
if type(replace_with) == "string" then
local remainder = itemstack:add_item(replace_with)
eater:set_wielded_item(itemstack)
if not remainder:is_empty() then
remainder = inv:add_item("main", replace_with)
if not remainder:is_empty() then
local pos = eater:get_pos()
minetest.add_item(pos, remainder)
end
end
else
for _, replace_item in ipairs(replace_with) do
local remainder = itemstack:add_item(replace_item)
eater:set_wielded_item(itemstack)
if not remainder:is_empty() then
remainder = inv:add_item("main", replace_item)
if not remainder:is_empty() then
local pos = eater:get_pos()
minetest.add_item(pos, remainder)
end
end
end
end
return itemstack
end
function balanced_diet.do_item_eat(itemstack, eater, pointed_thing)
if not minetest.is_player(eater) then
return
end
local def = itemstack:get_definition()
if not def then
return
end
local food_def = balanced_diet.get_food_def(itemstack)
if not food_def then
return
end
local player_name = eater:get_player_name()
local now = os.time()
local has_appetite, reason, eaten = balanced_diet.check_appetite_for(eater, itemstack, now)
if not has_appetite then
if reason then
minetest.chat_send_player(player_name, reason)
end
return
end
for _, callback in ipairs(balanced_diet.registered_on_item_eats) do
local result = callback(eater, itemstack, pointed_thing)
if result then
return result
end
end
if not minetest.is_creative_enabled(player_name) then
itemstack:take_item()
eater:set_wielded_item(itemstack)
end
local meta = eater:get_meta()
local food_name = itemstack:peek_item():to_string()
eaten[food_name] = food_def.duration
set_eaten(meta, eaten, now)
-- see https://github.com/minetest/minetest/pull/13286/files
if food_def.replace_with then
itemstack = handle_replace_with(eater, itemstack, food_def.replace_with)
end
if def.sound and def.sound.eat then
minetest.sound_play(def.sound.eat, { pos = eater:get_pos(), max_hear_distance = 16 }, true)
else
minetest.sound_play("balanced_diet_eat", { pos = eater:get_pos(), max_hear_distance = 16 }, true)
end
for _, callback in ipairs(balanced_diet.registered_after_item_eats) do
callback(eater, itemstack, pointed_thing)
end
return itemstack
end
function balanced_diet.item_eat()
return function(itemstack, eater, pointed_thing)
return balanced_diet.do_item_eat(itemstack, eater, pointed_thing)
end
end