212 lines
9.8 KiB
Lua
Executable File
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")
|