Files
2026-11-03 12:41:09 -05:00

259 lines
9.7 KiB
Lua
Executable File

-- Real-time hearth processes; construction supplies the shared access check.
return function(hearth, api, construction)
local allowed = construction.allowed
local MAX_FUEL = 600
-- Exact names only: building timber, charges and arbitrary tree groups are not fuel.
local FUEL = { ["hearth:sticks"] = 45, ["hearth:log"] = 180, ["hearth:charcoal"] = 90,
["hearth:coal"] = 240, ["hearth:oak_log"] = 180, ["hearth:birch_log"] = 180,
["hearth:pine_log"] = 180, ["hearth:willow_log"] = 180, ["hearth:alder_log"] = 180,
["hearth:beech_log"] = 180, ["hearth:aspen_log"] = 180, ["hearth:rowan_log"] = 180,
["hearth:hazel_log"] = 180, ["hearth:spruce_log"] = 180 }
local JOBS = {
["hearth:raw_food"] = {kind = "cook", duration = 6, cost = 20,
result = "hearth:cooked_food"},
["hearth:raw_fish"] = {kind = "roast", duration = 8, cost = 20,
result = "hearth:cooked_fish"},
["hearth:water_raw"] = {kind = "boil", duration = 4, cost = 10,
result = "hearth:water_clean"},
["hearth:smelt_charge"] = {kind = "smelt", duration = 60, cost = 30,
result = "hearth:iron_ingot"},
["hearth:copper_charge"] = {kind = "smelt_copper", duration = 45, cost = 30,
result = "hearth:copper_ingot"},
["hearth:dried_food_charge"] = {kind = "dry", duration = 300, cost = 30,
result = "hearth:dried_food"},
["hearth:clay_jar_charge"] = {kind = "fire", duration = 120, cost = 30,
result = "hearth:clay_jar"},
["hearth:charcoal_charge"] = {kind = "char", duration = 60, cost = 30,
result = "hearth:charcoal"},
["hearth:fish_drying_charge"] = {kind = "dry_fish", duration = 300, cost = 30,
result = "hearth:dried_fish"},
}
local RESULTS = { ["hearth:cooked_food"] = true, ["hearth:water_clean"] = true,
["hearth:iron_ingot"] = true, ["hearth:copper_ingot"] = true,
["hearth:dried_food"] = true, ["hearth:clay_jar"] = true,
["hearth:charcoal"] = true, ["hearth:cooked_fish"] = true, ["hearth:dried_fish"] = true }
local function is_fire(pos)
local name = api.get_node(pos).name
return name == "hearth:fire" or name == "hearth:fire_lit"
end
local function fuel_left(pos, now)
local deadline = tonumber(api.get_meta(pos):get_string("fuel_until")) or 0
if deadline ~= deadline or deadline == math.huge or deadline == -math.huge then return 0 end
return math.max(0, math.min(MAX_FUEL, deadline - now))
end
local function job_deadline(meta)
local raw = meta:get_string("job_until")
if raw == "" then return 0 end
local value = tonumber(raw)
if not value or value ~= value or value == math.huge or value == -math.huge then return nil end
return math.floor(value)
end
local function valid_job(meta)
local kind = meta:get_string("job_kind")
if kind == "" then return nil end
local until_time, cost = job_deadline(meta), meta:get_int("job_cost")
local result = meta:get_string("job_result")
if not until_time or until_time <= 0 then return nil end
for _, job in pairs(JOBS) do
if kind == job.kind and cost == job.cost and result == job.result then
return {kind = kind, until_time = until_time, cost = cost, result = result}
end
end
return nil
end
local function clear_job(meta)
meta:set_string("job_kind", "")
meta:set_string("job_until", "0")
meta:set_int("job_cost", 0)
end
local function pending_output(meta)
local result = meta:get_string("job_result")
if meta:get_string("job_kind") == "" and job_deadline(meta) == 0
and meta:get_int("job_cost") == 0 and RESULTS[result] then
return result
end
return ""
end
local function schedule_timer(pos, remaining, now)
local meta = api.get_meta(pos)
local wait
if remaining > 0 then wait = remaining end
local job = valid_job(meta)
if job then
local job_wait = job.until_time - now
if job_wait <= 0 then job_wait = 0.1 end
wait = wait and math.min(wait, job_wait) or job_wait
end
local timer = api.get_node_timer(pos)
if wait then
timer:start(math.max(0.1, wait))
else
timer:stop()
end
end
local function refresh_fire(pos, remaining, now)
local meta = api.get_meta(pos)
now = now or os.time()
remaining = remaining == nil and fuel_left(pos, now) or remaining
if remaining <= 0 then
remaining = 0
meta:set_string("fuel_until", "0")
meta:set_int("lit", 0)
end
local job = valid_job(meta)
local output = pending_output(meta)
local lit = meta:get_int("lit") == 1 and remaining > 0
meta:set_int("lit", lit and 1 or 0)
if output ~= "" and not job then
meta:set_string("infotext", "Last Hearth: finished " .. output .. " ready to collect")
elseif job then
local verb = ({cook = "cooking", roast = "roasting fish", boil = "boiling", smelt = "smelting iron",
smelt_copper = "smelting copper",
dry = "drying food", dry_fish = "drying fish", fire = "firing", char = "making charcoal"})[job.kind] or "processing"
meta:set_string("infotext", "Last Hearth: " .. verb .. "; " .. math.max(0,
math.ceil(job.until_time - now)) .. "s remaining")
elseif lit then
meta:set_string("infotext", "Last Hearth: lit; " .. math.ceil(remaining)
.. "s fuel; feed sticks/logs/charcoal/coal or process a charge")
elseif remaining > 0 then
meta:set_string("infotext", "Fueled but cold hearth: ignite with a firestarter")
else
meta:set_string("infotext", "Cold hearth: feed dry sticks or a log")
end
local node = api.get_node(pos)
local name = lit and "hearth:fire_lit" or "hearth:fire"
if node.name ~= name then
api.swap_node(pos, {name = name, param2 = node.param2})
end
schedule_timer(pos, remaining, now)
end
local function finish_job(_, meta, job)
clear_job(meta) -- Clear the reservation before committing its one output.
meta:set_string("job_result", job.result)
end
local function reconcile_job(pos, now)
local meta = api.get_meta(pos)
local kind = meta:get_string("job_kind")
if kind == "" then
local output = meta:get_string("job_result")
if (output ~= "" and pending_output(meta) == "")
or job_deadline(meta) ~= 0 or meta:get_int("job_cost") ~= 0 then
clear_job(meta)
meta:set_string("job_result", "")
end
return
end
local job = valid_job(meta)
if not job or job.until_time <= 0 then
clear_job(meta)
meta:set_string("job_result", "")
return
end
if job.until_time <= now then
finish_job(pos, meta, job)
end
end
local function fire_tick(pos)
if not is_fire(pos) then return false end
local now = os.time()
reconcile_job(pos, now)
refresh_fire(pos, fuel_left(pos, now), now)
return false -- refresh_fire schedules only the next job/fuel deadline.
end
local function use_fire(pos, _, player, stack)
if not is_fire(pos) or not allowed(pos, player, "use") then return stack end
local now = os.time()
local remaining = fuel_left(pos, now)
local meta = api.get_meta(pos)
if stack:is_empty() then
local result = pending_output(meta)
if result ~= "" then
local inv = player:get_inventory()
local output = ItemStack(result)
if inv and inv:room_for_item("main", output) then
local leftover = inv:add_item("main", output)
if leftover:is_empty() then meta:set_string("job_result", "") end
else
hearth.notify(player, "Make inventory space to collect the hearth output.")
end
end
refresh_fire(pos, remaining, now)
return stack
end
local item = stack:get_name()
local burn = FUEL[item]
if burn then
if remaining + burn <= MAX_FUEL then
stack:take_item(1)
remaining = remaining + burn
meta:set_string("fuel_until", tostring(now + remaining))
else
hearth.notify(player, "The hearth has no room for that much fuel yet.")
end
else
local job = JOBS[item]
if job then
if meta:get_string("job_kind") ~= "" or meta:get_string("job_result") ~= "" then
hearth.notify(player, "Collect the finished hearth output first.")
elseif meta:get_int("lit") ~= 1 then
hearth.notify(player, "Ignite the fueled hearth before cooking or boiling.")
elseif remaining < job.duration + job.cost then
hearth.notify(player, "Add more fuel to reserve the full hearth process.")
else
meta:set_string("job_kind", job.kind)
meta:set_string("job_until", tostring(now + job.duration))
meta:set_int("job_cost", job.cost)
meta:set_string("job_result", job.result)
-- Reserve heat cost at queue time; elapsed processing burns
-- naturally against the absolute UTC deadline. Paused servers do
-- not grant free fuel, but a completed offline job remains queued.
remaining = remaining - job.cost
meta:set_string("fuel_until", tostring(now + remaining))
-- Start the timer before consuming input; queued output is persistent.
schedule_timer(pos, remaining, now)
stack:take_item(1)
end
end
end
refresh_fire(pos, remaining, now)
return stack
end
local function ignite_fire(pos, player)
if not is_fire(pos) or not allowed(pos, player, "use") then return false end
local meta = api.get_meta(pos)
local now = os.time()
local remaining = fuel_left(pos, now)
if meta:get_int("lit") == 1 or remaining <= 0 then
refresh_fire(pos, remaining, now)
return false
end
meta:set_int("lit", 1)
refresh_fire(pos, remaining, now)
if api.sound_play then
api.sound_play("hearth_fire", {pos = pos, gain = 0.5, max_hear_distance = 16})
end
return true
end
local function use_firestarter(stack, player, pointed)
if pointed and pointed.type == "node" and ignite_fire(pointed.under, player) then
stack:add_wear(math.ceil(65535 / 20))
end
return stack
end
return {use_fire = use_fire, fire_tick = fire_tick,
refresh_fire = refresh_fire, fuel_left = fuel_left, ignite_fire = ignite_fire,
use_firestarter = use_firestarter}
end