Files
last-hearth/mods/hearth/init.lua
T
2026-11-03 12:41:09 -05:00

212 lines
9.8 KiB
Lua
Executable File

hearth = {players = {}, sessions = {}}
local path = core.get_modpath("hearth")
hearth.model = dofile(path .. "/model.lua")
-- Original texture generation: no downloaded assets or asset licensing ambiguity.
function hearth.texture(color)
return "hearth_" .. color:sub(2):lower() .. ".png"
end
function hearth.notify(player, message)
if player then core.chat_send_player(player:get_player_name(), message) end
end
local storage = core.get_mod_storage()
hearth.storage = storage
hearth.schema = dofile(path .. "/schema.lua")
hearth.persistence = dofile(path .. "/persistence.lua")(hearth.schema, core, storage)
hearth.movement = dofile(path .. "/movement.lua")
hearth.combat = dofile(path .. "/combat.lua")
-- A small claim registry is persisted in mod storage; never deserialize code.
local claims = hearth.persistence.load_world("claims")
local homes = hearth.persistence.load_world("homes")
local claim_access = hearth.persistence.load_world("access")
local access_policy = dofile(path .. "/access.lua")(claims, claim_access)
hearth.claim_owner_at = access_policy.owner_at
hearth.claim_role_at = access_policy.role_at
hearth.claim_allowed = access_policy.allowed
function hearth.home_for(name)
local home = homes[name]
return home and {x = home.x, y = home.y, z = home.z} or nil
end
function hearth.store_home(name, pos)
if type(name) ~= "string" or name == "" or not hearth.in_own_claim
or not hearth.in_own_claim(pos, name) then
error("Last Hearth: home save denied", 0)
end
local replacement = {}
for owner, home in pairs(homes) do replacement[owner] = {x = home.x, y = home.y, z = home.z} end
replacement[name] = {x = pos.x, y = pos.y, z = pos.z}
hearth.persistence.save_world("homes", replacement)
homes = replacement
end
function hearth.in_own_claim(pos, name)
return claims[name] ~= nil and hearth.claim_owner_at(pos) == name
end
local old_protected = core.is_protected
hearth.external_protected = old_protected
core.is_protected = function(pos, name)
return not hearth.claim_allowed(pos, name, "build") or old_protected(pos, name)
end
local function clone_positions(source)
local result = {}
for owner, pos in pairs(source) do result[owner] = {x = pos.x, y = pos.y, z = pos.z} end
return result
end
local function replace_map(target, source)
for key in pairs(target) do target[key] = nil end
for key, value in pairs(source) do
if type(value) == "table" then
local copy = {}
for k, v in pairs(value) do copy[k] = v end
target[key] = copy
else target[key] = value end
end
end
local function commit_claims(replacement)
hearth.persistence.save_world("claims", replacement)
replace_map(claims, replacement)
end
local function commit_access(replacement)
hearth.persistence.save_world("access", replacement)
replace_map(claim_access, replacement)
end
local function valid_player_name(value)
return type(value) == "string" and #value > 0 and #value <= 32
and value:match("^[%w_%-]+$") ~= nil
end
local function draw_claim(player, center)
if not core.add_particle then return false end
local name = player:get_player_name()
local function mark(x, z)
core.add_particle({pos = {x = x, y = center.y + 0.2, z = z},
velocity = {x = 0, y = 0.15, z = 0}, acceleration = {x = 0, y = 0, z = 0},
expirationtime = 6, size = 1.8, texture = hearth.texture("#e5c890"),
glow = 8, playername = name})
end
for offset = -12, 12, 2 do
mark(center.x + offset, center.z - 12); mark(center.x + offset, center.z + 12)
if offset > -12 and offset < 12 then
mark(center.x - 12, center.z + offset); mark(center.x + 12, center.z + offset)
end
end
return true
end
core.register_chatcommand("claim", {
params = "[show|list|share <player> <resident|storekeeper>|unshare <player>|remove]",
description = "Protect and manage a homestead; /claim show briefly marks its boundary.",
func = function(name, param)
local player = core.get_player_by_name(name)
if not player then return false, "Join the world first." end
if param == "show" or param == "list" then
local center = claims[name]
if not center then
center = hearth.claim_owner_at(player:get_pos()) and claims[hearth.claim_owner_at(player:get_pos())]
end
if not center then return false, "No homestead is under you. Use /claim to create one." end
if param == "show" then
if not hearth.claim_allowed(center, name, "use") then return false, "This homestead is private." end
draw_claim(player, center)
return true, string.format("Homestead bounds: X %d..%d, Z %d..%d, Y %d..%d.",
center.x - 12, center.x + 12, center.z - 12, center.z + 12, center.y - 24, center.y + 24)
end
if not claims[name] then return false, "Only the homestead owner can list its members." end
local members = claim_access[name] or {}
local names = {}
for member, role in pairs(members) do names[#names + 1] = member .. " (" .. role .. ")" end
table.sort(names)
return true, #names == 0 and "No shared members. Use /claim share <player> <resident|storekeeper>." or table.concat(names, ", ")
elseif param == "remove" then
if not claims[name] then return false, "You have no homestead to release." end
local new_access = {}
for owner, members in pairs(claim_access) do if owner ~= name then new_access[owner] = members end end
commit_access(new_access) -- Revoke first; a later failure cannot leave stale grants.
local new_homes = {}
for owner, home in pairs(homes) do if owner ~= name then new_homes[owner] = home end end
hearth.persistence.save_world("homes", new_homes); replace_map(homes, new_homes)
local new_claims = clone_positions(claims); new_claims[name] = nil
commit_claims(new_claims)
return true, "Homestead released; shared access and its saved home were removed."
elseif param:match("^share%s+") then
local member, role = param:match("^share%s+(%S+)%s+(%S+)$")
if not claims[name] then return false, "Create your homestead before sharing it." end
if not valid_player_name(member) or member == name or claims[member] then
return false, "Choose another valid player name; claim owners cannot be assigned as members."
end
if role ~= "resident" and role ~= "storekeeper" then
return false, "Role must be resident or storekeeper."
end
local new_access = {}
for owner, members in pairs(claim_access) do
new_access[owner] = {}; for who, value in pairs(members) do new_access[owner][who] = value end
end
new_access[name] = new_access[name] or {}
if not new_access[name][member] then
local count = 0; for _ in pairs(new_access[name]) do count = count + 1 end
if count >= 16 then return false, "A homestead can have at most 16 shared members." end
end
new_access[name][member] = role
commit_access(new_access)
return true, member .. " may access this homestead as " .. role .. "."
elseif param:match("^unshare%s+") then
local member = param:match("^unshare%s+(%S+)$")
if not claims[name] or not valid_player_name(member) then return false, "Use /claim unshare <player> from your own homestead." end
local new_access = {}
for owner, members in pairs(claim_access) do
new_access[owner] = {}; for who, value in pairs(members) do if not (owner == name and who == member) then new_access[owner][who] = value end end
if not next(new_access[owner]) then new_access[owner] = nil end
end
commit_access(new_access)
return true, member .. " no longer has access to your homestead."
elseif param == "" then
local p = vector.round(player:get_pos())
if claims[name] then return false, "Release your existing claim before moving it." end
for _, other in pairs(claims) do
if math.abs(p.x - other.x) <= 25 and math.abs(p.z - other.z) <= 25 then
return false, "Too close to another homestead. Move at least 26 blocks away."
end
end
local new_claims = clone_positions(claims)
new_claims[name] = {x = p.x, y = p.y, z = p.z}
commit_claims(new_claims)
draw_claim(player, p)
return true, "Homestead protected. Use /claim show to see its temporary boundary markers."
else return false, "Use /claim, /claim show, /claim list, /claim share, /claim unshare, or /claim remove." end
end,
})
local function homes_disabled()
return core.settings and core.settings.get_bool
and core.settings:get_bool("hearth_m1_course", false) == true
end
core.register_chatcommand("sethome", {
description = "Save a safe home inside your own homestead claim.",
func = function(name)
if homes_disabled() then return false, "Homes are disabled in the disposable M1 course. Use /course retry." end
local player = core.get_player_by_name(name)
if not player then return false, "Join the world first." end
if not hearth.set_home then return false, "Home services are still starting." end
return hearth.set_home(player)
end,
})
core.register_chatcommand("home", {
description = "Return to your verified personal home.",
func = function(name)
if homes_disabled() then return false, "Homes are disabled in the disposable M1 course. Use /course retry." end
local player = core.get_player_by_name(name)
if not player then return false, "Join the world first." end
if not hearth.go_home then return false, "Home services are still starting." end
return hearth.go_home(player)
end,
})
dofile(path .. "/content.lua")
hearth.character = dofile(path .. "/character.lua")(hearth, core)
dofile(path .. "/interface.lua")
dofile(path .. "/presentation.lua")
dofile(path .. "/feedback.lua")
dofile(path .. "/clock.lua")
dofile(path .. "/environment.lua")
dofile(path .. "/players.lua")
dofile(path .. "/runtime.lua")
dofile(path .. "/encounters.lua")
-- Enroll the fresh, nonce-marked fixture after core systems exist but before
-- mods-loaded / player callbacks. Course creation remains explicit /course consent.
dofile(path .. "/course.lua")