mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-24 14:02:11 +02:00
216 lines
12 KiB
Python
216 lines
12 KiB
Python
"""Scan clean-up on a temporary fake Frame: refine.json, the options every build passes, room_refine (a fake
|
|
fp_roomscan script writes the scanner interface's files) and the player's own furniture kept over a new clean-up."""
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from test_agent import load_agent
|
|
|
|
from frameport.install.rooms import REFINE_DEFAULTS
|
|
from frameport.mr.room import Mesh, Volume, box_room, dumps, parse, write_mesh
|
|
|
|
# A stand-in for fp_roomscan (scanner interface of docs/mr): --offline <dump> --out <dir> builds a room; --refine-only
|
|
# <dir> cleans one up again from room-raw.txt (an old room without raw files: room.txt becomes the raw one). It writes
|
|
# room.txt + room-mesh.bin (refined), room-raw.txt + room-mesh-raw.bin, scan.json, and logs its arguments.
|
|
FAKE_SCANNER = r'''#!PYTHON
|
|
import json, os, shutil, struct, sys, uuid
|
|
argv = sys.argv[1:]
|
|
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "scanner-calls.jsonl"), "a") as log:
|
|
log.write(json.dumps(sys.argv) + "\n")
|
|
def opt(name, default=None):
|
|
return argv[argv.index(name) + 1] if name in argv else default
|
|
if os.environ.get("FAKE_SCANNER_FAIL"):
|
|
sys.stderr.write("boom")
|
|
sys.exit(3)
|
|
mesh = (struct.pack("<4sIII", b"FPRM", 1, 3, 3) + struct.pack("<9f", 0, 0, 0, 1, 0, 0, 0, 0, 1)
|
|
+ struct.pack("<3I", 0, 1, 2))
|
|
if "--offline" in argv:
|
|
out = opt("--out")
|
|
universe = opt("--universe")
|
|
walls = ("wall {} -2 -1.5 2 -1.5 0 2.5\nwall {} 2 -1.5 2 1.5 0 2.5\n"
|
|
"wall {} 2 1.5 -2 1.5 0 2.5\nwall {} -2 1.5 -2 -1.5 0 2.5\n")
|
|
raw = ("frameport-room 1\nuniverse " + universe + "\nsource scan\ntime 100\nroom " + uuid.uuid4().hex +
|
|
"\nfloor " + uuid.uuid4().hex + " 0\nceiling " + uuid.uuid4().hex + " 2.5\n" +
|
|
walls.format(*(uuid.uuid4().hex for _ in range(4))) +
|
|
"volume " + uuid.uuid4().hex + " COUCH 0 1 0.75 0 1.5 0.6 0.75\n"
|
|
"mesh " + uuid.uuid4().hex + " room-mesh.bin 3 3\n")
|
|
open(os.path.join(out, "room-raw.txt"), "w").write(raw.replace("room-mesh.bin", "room-mesh-raw.bin"))
|
|
open(os.path.join(out, "room-mesh-raw.bin"), "wb").write(mesh)
|
|
src = raw
|
|
folder = out
|
|
else:
|
|
folder = opt("--refine-only")
|
|
if not os.path.exists(os.path.join(folder, "room-raw.txt")):
|
|
text = open(os.path.join(folder, "room.txt")).read()
|
|
open(os.path.join(folder, "room-raw.txt"), "w").write(text.replace("room-mesh.bin", "room-mesh-raw.bin"))
|
|
shutil.copy(os.path.join(folder, "room-mesh.bin"), os.path.join(folder, "room-mesh-raw.bin"))
|
|
src = open(os.path.join(folder, "room-raw.txt")).read().replace("room-mesh-raw.bin", "room-mesh.bin")
|
|
# the label rules: the couch is really a table (--label-rules 0 keeps the scanner's label)
|
|
rules = opt("--label-rules", "1") == "1"
|
|
refined = src.replace(" COUCH ", " TABLE ") if rules else src
|
|
open(os.path.join(folder, "room-mesh.bin"), "wb").write(mesh)
|
|
open(os.path.join(folder, "room.txt"), "w").write(refined)
|
|
settings = {"walls": opt("--walls", "panels"), "smooth": int(opt("--smooth", "8"))}
|
|
json.dump({"version": 1, "heads": [[0, 1.6, 0], [0.5, 1.6, 0]], "refine": settings,
|
|
"report": {"triangles": 1, "kept": 1, "walked": 0},
|
|
"labels": [{"from": "COUCH", "to": "TABLE" if rules else "COUCH", "reason": "top 0.75 m: a table",
|
|
"box": {"cx": 0, "cz": 1, "top_y": 0.75, "yaw_deg": 0, "w": 1.5, "d": 0.6, "h": 0.75}}],
|
|
"suggestions": [{"kind": "door", "box": {"cx": 2, "cz": 0, "top_y": 2, "yaw_deg": 90, "w": 0.9, "d": 0.1,
|
|
"h": 2}, "wall": 1, "along": 0.5}]},
|
|
open(os.path.join(folder, "scan.json"), "w"))
|
|
print(json.dumps({"triangles": 1, "refine": {"kept": 1}}))
|
|
'''
|
|
|
|
|
|
def scanner(a, monkeypatch):
|
|
path = Path(a.ROOM_SCANNER)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(FAKE_SCANNER.replace("PYTHON", sys.executable))
|
|
path.chmod(0o755)
|
|
monkeypatch.setattr(a, "room_current_universe", lambda: 7)
|
|
return path.parent / "scanner-calls.jsonl"
|
|
|
|
|
|
def calls(log):
|
|
return [json.loads(line) for line in log.read_text().splitlines()]
|
|
|
|
|
|
def test_defaults_match_the_pc_and_settings_validate(monkeypatch, tmp_path):
|
|
a = load_agent(monkeypatch, tmp_path)
|
|
assert a.ROOM_REFINE_DEFAULTS == {k: m["default"] for k, m in REFINE_DEFAULTS.items()}
|
|
assert set(a.ROOM_REFINE_LIMITS) | {"walls"} == set(REFINE_DEFAULTS)
|
|
for key, meta in REFINE_DEFAULTS.items(): # every slider value the PC offers is accepted
|
|
if meta["kind"] == "number":
|
|
lo, hi = a.ROOM_REFINE_LIMITS[key]
|
|
assert lo <= meta["min"] and meta["max"] <= hi, key
|
|
assert a.room_refine_settings({"band": 0.35, "walls": "snap", "smooth": 4.0, "protect": True}) == \
|
|
{"walls": "snap", "smooth": 4, "protect": 1}
|
|
for bad in ({"nope": 1}, {"walls": "maybe"}, {"band": float("nan")}, {"smooth": 2.5}, {"band": 9},
|
|
{"island": "big"}):
|
|
with pytest.raises(a.AgentError):
|
|
a.room_refine_settings(bad)
|
|
assert a.room_refine_args({"walls": "snap", "facing_deg": 45.0, "clip_open": 0.2}) == \
|
|
["--walls", "snap", "--facing-deg", "45", "--clip-open", "0.2"]
|
|
assert a.room_refine_args({}, [[0, 1, 2.5, 0.25], {"x": 1, "y": 0, "z": 0, "r": 0.1}])[-2:] == \
|
|
["--erase", "0,1,2.5,0.25;1,0,0,0.1"]
|
|
for bad in ([[0, 1, 2]], [[0, 1, 2, 0]], [["a", 1, 2, 3]]):
|
|
with pytest.raises(a.AgentError, match="erase"):
|
|
a.room_refine_args({}, bad)
|
|
assert a.room_refine_load() == {} # no file / a broken one = the defaults
|
|
Path(a.ROOMS_DIR).mkdir(parents=True)
|
|
Path(a.ROOM_REFINE_FILE).write_text("{broken")
|
|
assert a.room_refine_load() == {}
|
|
|
|
|
|
def test_build_passes_settings_and_keeps_raw_files(monkeypatch, tmp_path):
|
|
a = load_agent(monkeypatch, tmp_path)
|
|
log = scanner(a, monkeypatch)
|
|
Path(a.ROOMS_DIR).mkdir(parents=True)
|
|
dump = Path(a.ROOMS_DIR, "scan.fprs")
|
|
dump.write_bytes(b"FPRS")
|
|
status = a.cmd_room_build({"dump": str(dump)}) # nothing saved: no clean-up options (an old scanner works)
|
|
argv = calls(log)[-1]
|
|
assert argv[:3] == [a.ROOM_SCANNER, "--offline", str(dump)] and argv[argv.index("--universe") + 1] == "7"
|
|
assert "--walls" not in argv and argv[-1] == "--json"
|
|
folder = Path(a.ROOMS_DIR, "7")
|
|
assert {p.name for p in folder.iterdir()} >= {"room.txt", "room-mesh.bin", "room-raw.txt", "room-mesh-raw.bin",
|
|
"scan.json"}
|
|
assert status["report"] == {"triangles": 1, "refine": {"kept": 1}}
|
|
assert status["labels"][0]["to"] == "TABLE" and status["suggestions"][0]["kind"] == "door"
|
|
assert status["refine"] == {} and status["raw"] and not status["edited"]
|
|
assert status["room"]["summary"]["volumes"] == {"TABLE": 1}
|
|
a._write_json(a.ROOM_REFINE_FILE, {"walls": "snap", "smooth": 3})
|
|
a.cmd_room_build({"dump": str(dump)})
|
|
argv = calls(log)[-1]
|
|
assert argv[argv.index("--walls") + 1] == "snap" and argv[argv.index("--smooth") + 1] == "3"
|
|
|
|
|
|
def test_refine_saves_settings_republishes_and_keeps_edits(monkeypatch, tmp_path):
|
|
a = load_agent(monkeypatch, tmp_path)
|
|
log = scanner(a, monkeypatch)
|
|
with pytest.raises(a.AgentError, match="No room"):
|
|
a.cmd_room_refine({})
|
|
# an old room (no raw files): the scanner takes room.txt as the raw one
|
|
r = box_room(4, 3)
|
|
r.universe = 7
|
|
r.source = "scan"
|
|
r.volumes = [Volume("a" * 32, "COUCH", 0, 1, 0.75, 0, 1.5, 0.6, 0.75)]
|
|
m = Mesh(vertices=[(0, 0, 0), (1, 0, 0), (0, 0, 1)], indices=[0, 1, 2])
|
|
r.mesh = Mesh(vertex_count=3, index_count=3)
|
|
a.room_store(dumps(r), write_mesh(m), "old scan log")
|
|
status = a.cmd_room_refine({"settings": {"walls": "snap", "band": 0.35, "label_rules": 0}})
|
|
assert json.loads(Path(a.ROOM_REFINE_FILE).read_text()) == {"walls": "snap", "label_rules": 0}
|
|
argv = calls(log)[-1]
|
|
assert argv[:2] == [a.ROOM_SCANNER, "--refine-only"] and argv[-1] == "--json"
|
|
assert argv[argv.index("--walls") + 1] == "snap" and "--band" not in argv
|
|
assert status["summary"] == {"triangles": 1, "refine": {"kept": 1}}
|
|
assert status["refine"] == {"walls": "snap", "label_rules": 0} and status["raw"]
|
|
assert status["room"]["summary"]["volumes"] == {"COUCH": 1} # label rules off
|
|
folder = Path(a.ROOMS_DIR, "7")
|
|
assert (folder / "scan.log").read_text() == "old scan log" and (folder / "room-raw.txt").is_file()
|
|
assert not [n for n in os.listdir(a.ROOMS_DIR) if n.startswith(".refine-")] # no stage left behind
|
|
# null settings keep the saved ones; erase spots go to the scanner
|
|
a.cmd_room_refine({"settings": None, "erase": [[0, 1, 0, 0.3]]})
|
|
argv = calls(log)[-1]
|
|
assert argv[argv.index("--walls") + 1] == "snap" and argv[argv.index("--erase") + 1] == "0,1,0,0.3"
|
|
# the Room tab's box editor: an edited room keeps the raw files and its edits survive a new clean-up
|
|
edited = parse(status["room_text"])
|
|
edited.volumes = [Volume("b" * 32, "BED", 1, 0, 0.5, 0, 2, 1.4, 0.5)]
|
|
incoming = Path(a.ROOMS_DIR, ".incoming-x")
|
|
incoming.mkdir()
|
|
(incoming / "room.txt").write_text(dumps(edited))
|
|
(incoming / "room-mesh.bin").write_bytes(write_mesh(m))
|
|
a.cmd_room_put({"path": str(incoming / "room.txt"), "edited": True})
|
|
assert (folder / "edits.txt").read_text().startswith("volume " + "b" * 32 + " BED")
|
|
assert (folder / "room-raw.txt").is_file() and (folder / "scan.json").is_file()
|
|
status = a.cmd_room_refine({"settings": {}})
|
|
assert status["edited"] and status["room"]["summary"]["volumes"] == {"BED": 1}
|
|
assert status["refine"] == {} and json.loads(Path(a.ROOM_REFINE_FILE).read_text()) == {}
|
|
status = a.cmd_room_refine({"keep_edits": False}) # back to the scan's boxes
|
|
assert not status["edited"] and status["room"]["summary"]["volumes"] == {"TABLE": 1}
|
|
# a plain import (another room) doesn't keep this one's raw files
|
|
a.cmd_room_put({"path": str(incoming / "room.txt")})
|
|
assert not (folder / "room-raw.txt").exists() and not a.cmd_room_status({})["raw"]
|
|
|
|
|
|
def test_refine_failures_leave_the_room(monkeypatch, tmp_path):
|
|
a = load_agent(monkeypatch, tmp_path)
|
|
scanner(a, monkeypatch)
|
|
r = box_room(4, 3)
|
|
r.universe = 7
|
|
text = dumps(r)
|
|
a.room_store(text)
|
|
with pytest.raises(a.AgentError, match="unknown"):
|
|
a.cmd_room_refine({"settings": {"bogus": 1}})
|
|
assert not Path(a.ROOM_REFINE_FILE).exists()
|
|
monkeypatch.setenv("FAKE_SCANNER_FAIL", "1")
|
|
with pytest.raises(a.AgentError, match="clean-up failed: boom"):
|
|
a.cmd_room_refine({})
|
|
assert Path(a.ROOMS_DIR, "7", "room.txt").read_text() == text
|
|
monkeypatch.delenv("FAKE_SCANNER_FAIL")
|
|
import fcntl
|
|
with open(os.path.join(a.ROOMS_DIR, ".scan.lock"), "a") as lock: # a scan is building a room
|
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
with pytest.raises(a.AgentError, match="scan is running"):
|
|
a.cmd_room_refine({})
|
|
with pytest.raises(a.AgentError, match="unexpected"):
|
|
a.room_store(text, extras={"../evil": b""})
|
|
Path(a.ROOM_SCANNER).unlink()
|
|
with pytest.raises(a.AgentError, match="not installed"):
|
|
a.cmd_room_refine({})
|
|
|
|
|
|
def test_status_reads_scan_json_defensively(monkeypatch, tmp_path):
|
|
a = load_agent(monkeypatch, tmp_path)
|
|
monkeypatch.setattr(a, "room_current_universe", lambda: 7)
|
|
r = box_room(4, 3)
|
|
r.universe = 7
|
|
info = b'{"report": [1], "labels": {"x": 1}, "suggestions": [{"kind": "door"}]}'
|
|
a.room_store(dumps(r), extras={"scan.json": info})
|
|
st = a.cmd_room_status({})
|
|
assert st["report"] is None and st["labels"] == [] and st["suggestions"] == [{"kind": "door"}]
|
|
a.room_store(dumps(r), extras={"scan.json": b"[]"})
|
|
assert a.cmd_room_status({})["labels"] == []
|