-- Disposable M1 acceptance course. MAIN loads this before any player joins. -- Launcher contract (not implemented here): fresh HEARTH_HOME/course world, -- hearth_m1_course=true, hearth_m1_course_nonce=, and an exclusive -- world/.hearth-m1-course-nonce file containing that UUID plus optional LF. -- Use the summer fixture (UTC 1782043200, clear, latitude 45, longitude 0). -- A setting alone NEVER authorizes editing an existing survival world. local C, H = core, hearth if H.course then return H.course end -- One command, generation and sampler. local course = {} H.course = course local LOW, HIGH = {x = 0, y = 48, z = 0}, {x = 90, y = 58, z = 40} local START = {x = 5, y = 50.5, z = 5} local route = { {15, 51, 5, "Half steps / walk without jumping"}, {26, 50.5, 5, "One-wide, two-high doorway"}, {39, 49.5, 10, "Pool / enter, swim, exit both ends"}, {50, 50.5, 7, "Two-node fall / compare landing"}, {62, 50.5, 7, "Four-node fall / compare landing"}, {71, 50.5, 10, "Logs and flint / axe and hand gathering"}, {79, 50.5, 20, "Construction / wall, roof, support preview"}, {82, 50.5, 33, "Safe arena / opt in with /practice or /spar"}, {60, 50.5, 33, "Target lane / measure reach, dodge and brace"}, {8, 50.5, 33, "Finish / /course report; /course retry"}, } local kit = {"hearth:axe", "hearth:knife", "hearth:spear", "hearth:planks 64", "hearth:wall 64", "hearth:roof 64", "hearth:berries 12", "hearth:water_clean 6"} local runs, pending = {}, {} local phase, generation_failed, generation = "idle", false, 0 local nonce, enrolled local function finite(n) return type(n) == "number" and n == n and math.abs(n) < math.huge end local function time() return C.get_us_time() / 1000000 end local function feedback(player, message, kind) if H.feedback then H.feedback(player, message, kind or "info") end end local function environment_ok(name) local env = C.settings:get("hearth_environment") if env == nil then env = C.is_singleplayer() and "local" or "production" end if env == "production" then return false end -- Even server admins. if env == "local" then return C.is_singleplayer() == true end return env == "staging" and (name == nil or C.check_player_privs(name, {server = true}) == true) end local function uuid(value) return type(value) == "string" and #value == 36 and value:match("^%x%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$") ~= nil end local function marker_matches(value) local world = C.get_worldpath():gsub("\\", "/"):gsub("/+$", "") if world:match("([^/]+)$") ~= "course" then return false end -- No user-controlled filename, deserialization, code loading or unbounded read. local ok, result = pcall(function() local f = io.open(world .. "/.hearth-m1-course-nonce", "rb") if not f then return false end local bytes = f:read(65); f:close() return bytes == value or bytes == value .. "\n" end) return ok and result == true end local function configured() return C.settings:get_bool("hearth_m1_course") == true and environment_ok(nil) and uuid(nonce) and C.settings:get("hearth_m1_course_nonce") == nonce end -- Enroll ONLY on a fresh world's first mod load, before normal spawn/claims -- migration. Never mint a marker from /course or infer one from empty claims. nonce = C.settings:get("hearth_m1_course_nonce") if configured() and marker_matches(nonce) and H.storage then local storage = H.storage local existing = storage:get_string("m1_course_world_nonce") if existing == "" and not next(H.players) and not next(H.sessions) then local fresh = true for _, key in ipairs({"spawn", "claims", "location", "m0_backup:spawn", "m0_backup:claims", "m0_backup:location"}) do if storage:get_string(key) ~= "" then fresh = false end end if fresh then storage:set_string("m1_course_world_nonce", nonce) end end enrolled = storage:get_string("m1_course_world_nonce") == nonce end local function authorized(name) -- Recheck settings and privileges after asynchronous emergence as well. return enrolled == true and configured() and environment_ok(name) and H.storage:get_string("m1_course_world_nonce") == nonce end local function live(player, session) if not player or not player:is_player() then return false end local name = player:get_player_name() return C.get_player_by_name(name) == player and H.sessions[name] == session and H.players[name] ~= nil and session ~= nil and not session.waiting and player:get_hp() > 0 end local function inside(p) return p and finite(p.x) and finite(p.y) and finite(p.z) and p.x >= LOW.x and p.x <= HIGH.x and p.y >= LOW.y and p.y <= HIGH.y and p.z >= LOW.z and p.z <= HIGH.z end local function put(x, y, z, node) local p = {x = x, y = y, z = z} assert(inside(p), "Course write outside fixed bounds") C.set_node(p, {name = node}) end local slab = "hearth:m1_course_half_step" if enrolled then C.register_node(slab, {description = "Disposable course half step", drawtype = "nodebox", tiles = {H.texture("#a48a61")}, paramtype = "light", walkable = true, node_box = {type = "fixed", fixed = {-0.5, -0.5, -0.5, 0.5, 0, 0.5}}, groups = {choppy = 3, not_in_creative_inventory = 1}, drop = ""}) end local function generate() -- Full loaded preflight BEFORE the first write. The volume is fixed (41041 -- nodes); no terrain search, on_generated callback, or per-player generation. for x = LOW.x, HIGH.x do for z = LOW.z, HIGH.z do for y = LOW.y, HIGH.y do local n = C.get_node_or_nil({x = x, y = y, z = z}) if not n or n.name == "ignore" then return false end end end end for x = LOW.x, HIGH.x do for z = LOW.z, HIGH.z do for y = LOW.y, HIGH.y do local edge = x == 0 or x == 90 or z == 0 or z == 40 put(x, y, z, (y <= 50 or (edge and y <= 53)) and "hearth:stone" or "air") end end end -- .5-node rises, full-node steps and a descending ramp with no autojump need. for x = 11, 18 do for z = 4, 6 do local height = math.min(x - 10, 19 - x) for y = 51, 50 + math.ceil(height / 2) do put(x, y, z, height % 2 == 1 and y == 50 + math.ceil(height / 2) and slab or "hearth:planks") end end end -- Exactly one node wide, two clear nodes high; optional separate snag. for z = 2, 8 do for y = 51, 53 do if z ~= 5 or y == 53 then put(26, y, z, "hearth:wall") end end end if C.settings:get_bool("hearth_m1_course_snag") == true then for x = 28, 30 do put(x, 52, 9, "hearth:roof") end end -- Contained two-deep pool, one-deep entry/exit shelves. for x = 34, 44 do for z = 7, 13 do for y = (x == 34 or x == 44) and 50 or 49, 50 do put(x, y, z, "hearth:water_source") end end end -- Approach ramps to fall platforms, then open landing pads at floor level. for _, fall in ipairs({{46, 2}, {56, 4}}) do for i = 0, fall[2] do for z = 6, 8 do for y = 51, 50 + i do put(fall[1] + i, y, z, "hearth:planks") end end end end for x = 68, 72, 2 do put(x, 51, 10, "hearth:log"); put(x, 51, 12, "hearth:loose_stone") end -- Distinct construction anchor; arena/target lane remain clear and dry. put(77, 51, 20, "hearth:wall") for x = 8, 66, 2 do put(x, 50, 33, "hearth:planks") end for x = 73, 87 do for z = 28, 38 do put(x, 50, z, "hearth:planks") end end return true end local function append(buffer, value) buffer.index = buffer.index % 600 + 1 buffer[buffer.index] = value buffer.count = math.min(600, buffer.count + 1) end local function samples() return {index = 0, count = 0} end local function percentile(buffer) if buffer.count == 0 then return "n/a" end local sorted = {} for i = 1, buffer.count do sorted[i] = buffer[i] end table.sort(sorted) return string.format("%.2f", sorted[math.ceil(#sorted * 0.95)]) end function course.report(name) local run = runs[name] if not run then return "No course run in this session. Use /course." end return string.format("Course %s: %.1fs; checkpoints %d/%d. Engine globalstep dt p95 %sms (last %d samples); course Lua callback wall duration p95 %sus. Not client FPS or CPU profiling. Hits: not instrumented.", run.finished and "complete" or "running", (run.finished or time()) - run.started, run.checkpoint, #route, percentile(run.dt), run.dt.count, percentile(run.lua)) end local function begin(player, session) local name = player:get_player_name() if not authorized(name) or not live(player, session) then return end -- Intentionally replaces the kit/needs only in the enrolled disposable world. player:get_inventory():set_list("main", kit) for key, value in pairs({food = 100, water = 100, warmth = 75, wet = 0, stamina = 100}) do H.players[name][key] = value end player:set_hp(20, {type = "set_hp", from = "mod"}) player:set_pos({x = START.x, y = START.y, z = START.z}) player:set_velocity({x = 0, y = 0, z = 0}) -- Keep session combat cooldowns intact; teleport sampling is handled by combat. runs[name] = {player = player, session = session, started = time(), checkpoint = 0, dt = samples(), lua = samples(), next_check = 0} feedback(player, "Course started / east: half steps, doorway, pool; /course report", "success") C.chat_send_player(name, "Disposable course kit. Walk east through steps/doorway/pool, 2/4-node falls, logs/flint, build pad, arena then west along target lane. Use /practice or /spar explicitly; no automatic opponent. /course report shows server dt, not FPS; /course retry resets kit/needs/timer. Optional snag is at (29,51,9).") end local function request(player, session) local name = player:get_player_name() if phase == "ready" then begin(player, session); return true, "Course started. /course report for telemetry." end if phase == "failed" then return false, "Course generation failed; relaunch a fresh disposable course world." end pending[name] = {player = player, session = session} if phase == "loading" then return true, "Course emergence pending." end phase, generation_failed = "loading", false generation = generation + 1 local ticket = generation C.emerge_area(LOW, HIGH, function(_, action, left) if phase ~= "loading" or ticket ~= generation then return end if action == (C.EMERGE_CANCELLED or 0) or action == (C.EMERGE_ERRORED or 1) then generation_failed = true end if left ~= 0 then return end -- An opt-in request must still be live and authorized before ANY writes. local consent = false for n, entry in pairs(pending) do if authorized(n) and live(entry.player, entry.session) then consent = true end end if not consent then pending, phase = {}, "idle"; return end local ok, built = false, false if not generation_failed then ok, built = pcall(generate) end phase = ok and built and "ready" or "failed" local queued = pending; pending = {} for n, entry in pairs(queued) do if authorized(n) and live(entry.player, entry.session) then if phase == "ready" then begin(entry.player, entry.session) else C.chat_send_player(n, "Course generation failed; relaunch a fresh disposable course world.") end end end end) return true, "Preparing bounded disposable course; inventory changes only after emergence." end C.register_chatcommand("course", { params = "[retry|report]", description = "Opt in to the disposable M1 course (never production)", func = function(name, param) if not authorized(name) then return false, "Course denied: launch a fresh marked local/staging course world; staging requires server privilege." end local player, session = C.get_player_by_name(name), H.sessions[name] if not live(player, session) then return false, "Join, finish arrival and be alive before opting in." end if param == "report" then return true, course.report(name) end if param ~= "" and param ~= "retry" then return false, "Use /course, /course retry or /course report." end local count = 0 for _ in pairs(runs) do count = count + 1 end for n in pairs(pending) do if not runs[n] then count = count + 1 end end if not runs[name] and not pending[name] and count >= 16 then return false, "Course session limit reached (16)." end return request(player, session) end, }) C.register_globalstep(function(dt) if not finite(dt) or dt <= 0 then return end for name, run in pairs(runs) do if not authorized(name) or not live(run.player, run.session) then runs[name] = nil elseif not run.finished then local before = C.get_us_time() local t = before / 1000000 append(run.dt, dt * 1000) -- Engine-provided step interval, NOT Lua CPU time. if t - run.started > 1800 then runs[name] = nil elseif t >= run.next_check then run.next_check = t + 0.1 -- No catch-up loop or terrain work in globalstep. local p, checkpoint = run.player:get_pos(), route[run.checkpoint + 1] if inside(p) and (p.x-checkpoint[1])^2 + (p.y-checkpoint[2])^2 + (p.z-checkpoint[3])^2 <= 25 then run.checkpoint = run.checkpoint + 1 feedback(run.player, "Checkpoint " .. run.checkpoint .. "/" .. #route .. " / " .. checkpoint[4], "success") if run.checkpoint == #route then run.finished = t end end end append(run.lua, math.max(0, C.get_us_time() - before)) end end end) C.register_on_leaveplayer(function(player) local name = player:get_player_name(); runs[name], pending[name] = nil, nil end) C.register_on_dieplayer(function(player) local name = player:get_player_name(); runs[name], pending[name] = nil, nil end) return course