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 |unshare |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 ." 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 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")