-- Offline Lua 5.1+ factory tests. Engine JSON is represented by exact JSON -- fixture strings, not executable deserialization or a fake browser service. local factory = dofile(assert(arg[1], "character.lua path required")) local total = 0 local function test(name, fn) local ok, err = pcall(fn); assert(ok, name .. ": " .. tostring(err)) total = total + 1; print("PASS " .. name) end local function copy(t) local o = {}; for k,v in pairs(t) do o[k] = v end; return o end local function harness() local h = {sessions = {}, messages = {}, screens = {}, props = {}, closed = {}, commands = {}, connected = {}, json = {}, parses = 0} local api = {} function api.write_json(value) if h.encode_fail then error("PRIVATE encoder detail") end local fields = {} for k,v in pairs(value) do fields[#fields+1] = '"' .. k .. '":' .. (type(v)=="number" and v or '"' .. tostring(v) .. '"') end table.sort(fields) local raw = "{" .. table.concat(fields,",") .. "}" h.json[raw] = copy(value); return raw end function api.parse_json(raw, null, return_error) h.parses = h.parses + 1; assert(null == nil and return_error == true) if h.parse_throw then error("PRIVATE parse detail") end return h.json[raw] and copy(h.json[raw]) or nil end function api.formspec_escape(s) return (s:gsub("\\","\\\\"):gsub("%[","\\["):gsub("%]","\\]"):gsub(";","\\;"):gsub(",","\\,")) end function api.get_player_by_name(name) return h.connected[name] end function api.register_on_player_receive_fields(fn) h.fields = fn end function api.register_chatcommand(name, def) h.commands[name] = def end function api.show_formspec(name, form, spec) h.screens[#h.screens+1] = {name=name,form=form,spec=spec} end function api.close_formspec(name, form) h.closed[#h.closed+1] = {name,form} end function h.notify(_, message) h.messages[#h.messages+1] = message end function h.player(name) local meta = {raw = "", writes = 0} function meta:get_string(k) assert(k == "hearth:appearance"); if self.read_fail then error("PRIVATE read") end; return self.raw end function meta:set_string(k, v) assert(k == "hearth:appearance"); self.writes = self.writes + 1 if self.fail then error("PRIVATE write") end if self.reject then return false end self.raw = v end local p = {name=name or "alice", meta=meta, properties={}} function p:get_player_name() return self.name end function p:get_meta() return self.meta end function p:set_properties(props) for k in pairs(props) do assert(k == "mesh" or k == "textures", "cosmetics cannot alter " .. k) end h.props[#h.props+1] = props; self.properties = props end h.sessions[p.name], h.connected[p.name] = {}, p return p, meta end h.api, h.C = api, factory(h,api) return h end test("factory exports fresh defaults and exact independent option lists", function() local h = harness(); local C = h.C local counts = {body=5,skin=6,face=5,eyes=5,hair=10,hairColor=6,beard=7,garment=7,cloth=8, trousers=4,boots=3,accent=9,accentColor=8,detail=5} for k,n in pairs(counts) do assert(#C.options()[k]==n,k) end local d, o = C.defaults(), C.options(); d.skin="BAD"; o.skin[1]="BAD" assert(C.defaults().skin=="sand" and C.options().skin[1]=="porcelain" and C.VERSION==1) end) test("strict schema rejects missing extra typed version and untrusted selections", function() local C = harness().C for _,bad in ipairs({false, 1, "{}", {}}) do assert(not C.validate(bad)) end for _,k in ipairs({"version","body","skin","face","eyes","hair","hairColor","beard","garment","cloth","trousers","boots","accent","accentColor","detail"}) do local d = C.defaults(); d[k]=nil; assert(not C.validate(d),k) end for _,v in ipairs({2,"1",false,{}}) do local d=C.defaults(); d.version=v; assert(not C.validate(d)) end for _,v in ipairs({"../skin.png","#ffffff","sand^other.png", "];button[0,0;", "https://invalid.example/skin", "", false, {}}) do local d=C.defaults(); d.skin=v; assert(not C.validate(d)) end for _,k in ipairs({"name","player","texture","__index",1}) do local d=C.defaults(); d[k]="x"; assert(not C.validate(d)) end assert(not C.validate(setmetatable(C.defaults(),{}))) end) test("all option values validate and map only to fixed mesh and layer filenames", function() local C=harness().C for k,list in pairs(C.options()) do for _,id in ipairs(list) do local d=C.defaults(); d[k]=id; assert(C.validate(d)) local mesh, texture=C.preview(d) assert(mesh:match("^hearth_person[_%a]*%.b3d$")) local n=0; for layer in texture:gmatch("[^%^]+") do assert(layer:match("^hearth_char_[%a_]+%.png$")); n=n+1 end assert(n==10) end end end) test("preview exactly follows opaque skin face eyes clothing accent hair beard detail order", function() local C=harness().C; local mesh,texture=C.preview(C.defaults()) assert(mesh=="hearth_person.b3d") assert(texture=="hearth_char_skin_sand.png^hearth_char_face_calm.png^hearth_char_eyes_hazel.png^hearth_char_garment_tunic_teal.png^hearth_char_trousers_earth.png^hearth_char_boots_walnut.png^hearth_char_hair_sidepart_chestnut.png^hearth_char_beard_none_chestnut.png^hearth_char_accent_scarf_sage.png^hearth_char_detail_none.png") assert(not C.preview({})) end) test("bounded silent JSON roundtrip refuses malformed oversize nested and invalid data", function() local h=harness(); local C=h.C; local d=C.defaults(); local raw=assert(C.encode(d)) assert(#raw<2048 and C.decode(raw).hair==d.hair) for _,raw_bad in ipairs({"", "{oops", "null", "[]", '{"skin":{}}', string.rep("{",1000), string.rep("x",2049)}) do assert(not C.decode(raw_bad)) end local before=h.parses; assert(not C.decode(string.rep("x",2049))); assert(h.parses==before) d.extra=true; assert(not C.decode(h.api.write_json(d))) h.parse_throw=true; assert(not C.decode(raw)) h.parse_throw=false; h.encode_fail=true; assert(not C.encode(C.defaults())) end) test("join applies defaults without saving or opening a form", function() local h=harness(); local p,m=h.player(); assert(h.C.on_join(p)) assert(m.writes==0 and #h.screens==0 and #h.messages==0 and #h.props==1) assert(not h.C.state(p).warning) end) test("saved JSON restores selected profile and appearance without a rewrite", function() local h=harness(); local p,m=h.player(); local d=h.C.defaults(); d.body="broad"; d.skin="ebony" m.raw=assert(h.C.encode(d)); assert(h.C.apply(p)) assert(p.properties.mesh=="hearth_person_broad.b3d" and p.properties.textures[1]:find("skin_ebony",1,true)) assert(m.writes==0 and not h.C.state(p).warning and #h.messages==0) end) test("corrupt saved bytes and read errors use defaults warn safely and never kick or overwrite", function() for _,raw in ipairs({"PRIVATE malformed",string.rep("x",2049)}) do local h=harness(); local p,m=h.player(); m.raw=raw; assert(h.C.on_join(p)) assert(m.raw==raw and m.writes==0 and #h.messages==1 and h.C.state(p).warning) assert(not h.messages[1]:find("PRIVATE",1,true)); h.C.show(p) assert(h.screens[1].spec:find("original data is retained",1,true)) end local h=harness(); local p,m=h.player(); m.read_fail=true; assert(h.C.on_join(p) and m.writes==0) end) test("command requires connected own session and other forms are ignored", function() local h=harness(); assert(not h.commands.character.func("offline")) local p=h.player(); assert(h.commands.character.func("alice")) assert(h.screens[1].name=="alice" and h.screens[1].form==h.C.FORM) assert(h.fields(p,"hearth:location",{apply=true})==nil) h.sessions.alice=nil; assert(not h.commands.character.func("alice")) end) test("four tab layout contains real escaped one-texture model and bounded controls", function() local h=harness(); local p=h.player(); h.C.show(p) for i=1,4 do h.C.handle(p,{["tab_"..i]=true}); local spec=h.screens[#h.screens].spec assert(spec:find("size[14,11]",1,true) and spec:find("model[0.65,1.65;5.1,6.4;traveler;hearth_person.b3d;",1,true)) assert(spec:find(";0,180;true;true;0,19;12]",1,true)) assert(spec:find("Face]",1,true) and spec:find("Hair]",1,true) and spec:find("Clothing]",1,true) and spec:find("Finishing]",1,true)) assert(not spec:find("http",1,true) and not spec:find("hearth_person.png",1,true)) for x,y,w,ht in spec:gmatch("button%[([%d.]+),([%d.]+);([%d.]+),([%d.]+);") do assert(tonumber(x)+tonumber(w)<=14 and tonumber(y)+tonumber(ht)<=11) end end assert(h.screens[#h.screens].spec:find("Freckles and scar",1,true)) -- Escape is applied to texture names too, rather than assuming safe output. local calls=0; local original=h.api.formspec_escape h.api.formspec_escape=function(s) calls=calls+1; return original(s) end h.C.show(p); assert(calls>10) end) test("draft tabs and rotations never publish player properties or metadata", function() local h=harness(); local p,m=h.player(); h.C.show(p); local before=#h.props assert(h.C.handle(p,{pick_body="1",pick_skin="6",tab_2=true})) assert(h.C.handle(p,{pick_hair="5",pick_hairColor="6",pick_beard="5"})) assert(h.C.handle(p,{back=true}) and h.C.state(p).yaw==0) assert(h.C.handle(p,{profile=true}) and h.C.state(p).yaw==90) assert(h.C.handle(p,{front=true}) and h.C.state(p).yaw==180) assert(h.C.state(p).draft.skin=="ebony" and h.C.state(p).draft.hair=="long") assert(#h.props==before and m.writes==0) end) test("two-digit dropdown indices select the expanded cosmetic catalogue safely", function() local h=harness(); local p=h.player(); h.C.show(p) assert(#h.C.options().hair==10) assert(h.C.handle(p,{tab_2=true})) assert(h.C.handle(p,{pick_hair="10",pick_hairColor="6",pick_beard="7"})) assert(h.C.state(p).draft.hair=="undercut" and h.C.state(p).draft.beard=="braided_beard") assert(not h.C.handle(p,{pick_hair="00"})) end) test("cancel and Escape discard draft preserving saved appearance and persistence", function() for _,field in ipairs({"cancel","quit"}) do local h=harness(); local p,m=h.player(); h.C.show(p); local old=p.properties.textures[1] h.C.handle(p,{pick_skin="6"}); h.C.handle(p,{[field]=true}) assert(not h.C.state(p).draft and m.writes==0 and p.properties.textures[1]==old) h.C.show(p); assert(h.C.state(p).draft.skin=="sand") end end) test("apply writes first then publishes and saved data survives a new session", function() local h=harness(); local p,m=h.player(); h.C.show(p) local original=p.set_properties p.set_properties=function(self,props) assert(m.writes==1 and h.C.decode(m.raw).skin=="ebony"); original(self,props) end assert(h.C.handle(p,{pick_skin="6",pick_body="4",apply=true})) assert(m.writes==1 and not h.C.state(p).draft and p.properties.mesh=="hearth_person_broad.b3d") p.set_properties=original; h.sessions.alice={}; h.C.on_join(p) assert(h.C.state(p).saved.skin=="ebony" and m.writes==1 and not h.C.state(p).warning) end) test("failed metadata or serialization leaves old published appearance and saved bytes", function() for _,mode in ipairs({"throw","false","encode"}) do local h=harness(); local p,m=h.player(); h.C.show(p); local old=p.properties.textures[1] m.fail=mode=="throw"; m.reject=mode=="false"; h.encode_fail=mode=="encode" h.C.handle(p,{pick_skin="6",apply=true}) assert(m.raw=="" and p.properties.textures[1]==old and h.C.state(p).saved.skin=="sand") assert(h.C.state(p).draft.skin=="ebony" and h.C.state(p).warning:find("could not be saved",1,true)) assert(not h.screens[#h.screens].spec:find("PRIVATE",1,true)) end end) test("corrupt record is replaced only by explicit successful Apply", function() local h=harness(); local p,m=h.player(); m.raw="PRIVATE bad" h.C.show(p); h.C.handle(p,{reset=true}); h.C.handle(p,{cancel=true}); assert(m.raw=="PRIVATE bad") h.C.show(p); h.C.handle(p,{apply=true}); assert(h.C.decode(m.raw) and m.writes==1 and not h.C.state(p).warning) end) test("reset and randomization use every allowlist without mutating defaults or publishing", function() local h=harness(); local p,m=h.player(); h.C.show(p); local before=#h.props for i=1,40 do h.C.handle(p,{randomize=true}); assert(h.C.validate(h.C.state(p).draft)) end h.C.handle(p,{reset=true}); local d=h.C.state(p).draft for k,v in pairs(h.C.defaults()) do assert(d[k]==v) end assert(m.writes==0 and #h.props==before) d.skin="BAD"; assert(h.C.state(p).draft.skin=="sand") end) test("forged fields indices hidden selections and target identities reject whole event", function() local h=harness(); local p,m=h.player(); local q,n=h.player("bob"); h.C.show(p); h.C.show(q) local cases={{pick_skin="0"},{pick_skin="7"},{pick_skin="01"},{pick_skin="1.0"},{pick_skin="NaN"}, {pick_skin={}}, {pick_skin=string.rep("1",2049)}, {pick_skin="];button[0,0;"}, {player="bob",apply=true,pick_skin="6"}, {pick_hair="1",apply=true}, {texture="x",apply=true}} for _,f in ipairs(cases) do assert(not h.C.handle(p,f)); assert(h.C.state(p).draft.skin=="sand") end assert(m.writes==0 and n.writes==0 and h.C.state(q).draft.skin=="sand") assert(not h.C.handle({get_player_name=function() return "alice" end},{apply=true})) end) test("stale sessions reconnects closed drafts and disconnected objects cannot save", function() local h=harness(); local p,m=h.player(); h.C.show(p); h.sessions.alice={} assert(not h.C.handle(p,{apply=true}) and m.writes==0) h.C.show(p); h.connected.alice=nil; assert(not h.C.handle(p,{apply=true})) h.connected.alice=p; h.C.show(p); h.C.handle(p,{cancel=true}); assert(not h.C.handle(p,{apply=true})) end) test("creator tooltip text passes through escaping without changing model semantics",function() local h=harness(); local p=h.player(); local seen={} local escape=h.api.formspec_escape h.api.formspec_escape=function(s) seen[s]=true; return escape(s) end h.C.show(p) local text="Drag to turn the preview. Only this model rotates." assert(seen[text] and h.screens[#h.screens].spec:find("tooltip[traveler;"..escape(text).."]",1,true)) assert(h.screens[#h.screens].spec:find(";0,180;true;true;0,19;12]",1,true)) end) assert(total==20,"Unexpected character test count") print("PASS " .. total .. " character creator tests")