用户要求:Create 与 Aeronautics 会引入大量原版没有的方块与危险区,
寻路必须一起优化。用户裁定:危险区用「表 + 命名推测并标记」,
动态机械用「警告 + 提高代价」。
## ★ 关键实测结论(本轮最重要)
我先按「给 mod 方块建语义表」做,实测覆盖率:
Create 643 个方块 → 显式表命中 9、命名规则推测 21、完全不识 613
覆盖率 4.7%
⇒ **手写表是死路**(64 mod / 1465 方块永远追不上)。
⇒ 但寻路真正需要的属性,**运行时全都有**:
Java 侧 blockinfo 已在读 BuiltInRegistries.BLOCK(**含 mod 运行时注册的方块**),
并返回 isSolid / destroySpeed / lightEmission / isAir / hasBlockEntity。
⇒ 正确做法是**问**,不是猜。
## 改动
新增 mcfold/solid.py:
- SolidSource 抽象:RegistrySolidSource(本机,快但无 mod)
vs RuntimeSolidSource(问 JVM,**含 mod**,准但要 JVM)
- 按 **state_id** 缓存(属性只依赖 state,不依赖坐标)
- attach_runtime() 批量探测(一次问一格要一次往返,批量才可用)
mcfold/pathfind.py:Terrain 改为「**先运行时缓存,再本机注册表**」;
can_dig 用 destroySpeed 判可挖(基岩官方约定是 -1 = 不可破坏),
不再靠名字猜「基岩挖不动」
新增 mcfold/modblocks.py(危险/会动这部分的表,配套覆盖率报告):
- 语义取���:ground/passable/hazard/breakable/solid/dynamic/carrier/unknown
- 三级来源:显式表 → 命名规则(**标记 inferred**)→ **如实说不认识**
- ★ vanilla 与 mod **分开算覆盖率**:vanilla 语义由注册表给,
把它算成「未知」会让人误以为「连石头都认不出」
WorldState:加 solid_cache 字段
## 判据
test_pathfind 19 → 24 项,新增 5 项:
- mod 方块优先用运行时 isSolid(本机注册表确实认不出)
- 不可破坏吃 destroySpeed(含基岩 -1 约定)
- 两边都无数据 ⇒ 不知道(**不谎称可走**)
- 覆盖率报告如实(且区分 mod/vanilla)
- 语义查询永远返回带 inferred 标记的结构
★ 写判据时踩了两个坑(都是 fixture 形状错,不是代码错):
1. `Chunk.sections[i]` 对应 y = MIN_Y + 16*i,MIN_Y=-64
⇒ 我按「第 0 个 section = y=0」写,查 y=0 拿到的是空气
2. 同一 section 内 16 格**只能一个值** ⇒ mod 方块与空气落同一 section 时,
空气格会变成 mod 方块(看着像代码 bug,其实是 fixture 形状问题)
⇒ 教训:**判据的 fixture 形状必须与被测代码的真实输入结构一致**。
226 项全通过。
249 lines
10 KiB
Python
249 lines
10 KiB
Python
"""M10b 加载器服务端判据:servers.py。
|
||
|
||
## 判据要点
|
||
|
||
1. **隔离**:mods 落**实例层**,内容根**零写入**
|
||
(这是 `test_version_isolation` 守的同一条不变量,加载器不能破坏它)
|
||
2. **未接的加载器要明说**,不能瞎猜或静默降级成 vanilla
|
||
3. **缺文件要指名道姓**,不能吞掉
|
||
4. 全程用**临时目录**,不碰真实 mcview
|
||
|
||
## 不测什么
|
||
|
||
下载安装器、起 JVM —— 那些需要网络/环境,放真实使用时验。
|
||
"""
|
||
import sys, os, json, tempfile, stat
|
||
from pathlib import Path
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
||
import mcfold.servers as S
|
||
|
||
|
||
def _tmp():
|
||
"""把模块的路径常量指到临时目录(隔离真实 mcview)。"""
|
||
tmp = Path(tempfile.mkdtemp(prefix="mcfold-srv-test-"))
|
||
S.BASE = tmp
|
||
S.STORE = tmp / "store"
|
||
S.INSTANCES = tmp / "instances"
|
||
S.RUNTIME = tmp / "runtime"
|
||
S.LOADER_CACHE = tmp / "loaders"
|
||
return tmp
|
||
|
||
|
||
def _fake_jar(tmp: Path, name: str = "server.jar", size: int = 5000) -> Path:
|
||
p = tmp / name
|
||
p.write_bytes(b"PK\x03\x04" + b"0" * size)
|
||
return p
|
||
|
||
|
||
# ---------- ① 隔离:mods 在实例层,内容根零写入 ----------
|
||
|
||
|
||
def test_mods_land_in_instance_not_content_root():
|
||
"""★★ mods 必须落**实例层**;内容根保持只读共享。
|
||
|
||
为什么这条是硬要求:一个 vanilla 内容根旁边要能起多个「装了不同 mod 的服」。
|
||
mods 若写进内容根 ⇒ 那些服会互相看到对方的 mod(这正是版本隔离要防的崩因)。
|
||
"""
|
||
tmp = _tmp()
|
||
jar = _fake_jar(tmp)
|
||
mod = _fake_jar(tmp, "coolmod.jar")
|
||
r = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
|
||
mod=[str(mod)], server_jar=str(jar))
|
||
assert r["ok"] is True, r
|
||
inst = Path(r["instance"])
|
||
assert (inst / "mods" / "coolmod.jar").exists(), "mod 应在实例的 mods/ 里"
|
||
# ★ 内容根零写入
|
||
assert not S.STORE.exists() or not any(S.STORE.iterdir()), \
|
||
f"内容根被写入了:{list(S.STORE.iterdir()) if S.STORE.exists() else []}"
|
||
print(" ★★ mods 落实例层,内容根零写入")
|
||
|
||
|
||
def test_two_instances_share_content_root_different_mods():
|
||
"""★ 同一版本的两个实例可装**不同** mod(多实例的意义)。"""
|
||
tmp = _tmp()
|
||
jar = _fake_jar(tmp)
|
||
a = _fake_jar(tmp, "modA.jar")
|
||
b = _fake_jar(tmp, "modB.jar")
|
||
r1 = S.create_modded_server("instA", version="1.21.1", loader="vanilla",
|
||
mod=[str(a)], server_jar=str(jar))
|
||
r2 = S.create_modded_server("instB", version="1.21.1", loader="vanilla",
|
||
mod=[str(b)], server_jar=str(jar))
|
||
assert r1["ok"] and r2["ok"]
|
||
ma = sorted(p.name for p in (Path(r1["instance"]) / "mods").iterdir())
|
||
mb = sorted(p.name for p in (Path(r2["instance"]) / "mods").iterdir())
|
||
assert ma == ["modA.jar"] and mb == ["modB.jar"], (ma, mb)
|
||
print(" ★ 同版本两实例各装各的 mod(互不看见)")
|
||
|
||
|
||
def test_instance_json_records_version_and_loader(monkey=None):
|
||
"""★ 实例元数据要记 version/loader —— 隔离的**依据**。
|
||
|
||
(之前 7 个实例里有 2 个缺 instance.json,list_instances 只能报
|
||
has_meta: False ⇒ 隔离没有可核查的依据。)
|
||
|
||
★ fabric 路径会去**下载** server-launch ⇒ 判据里替换掉下载函数,
|
||
避免判据依赖网络(网络一挂就假红)。
|
||
"""
|
||
tmp = _tmp()
|
||
jar = _fake_jar(tmp)
|
||
# ★ fabric 现在走 classpath 启动(实测 maven 上没有 server-launch 产物)
|
||
# ⇒ 判据替换「库解析」,不依赖网络与本地资产
|
||
libs = tmp / "fakelibs"
|
||
libs.mkdir()
|
||
for n in ("fabric-loader.jar", "intermediary.jar"):
|
||
(libs / n).write_bytes(b"PK\x03\x04" + b"0" * 5000)
|
||
orig = S.fabric_libraries
|
||
S.fabric_libraries = lambda v, loader_version="": {
|
||
"ok": True, "jars": [str(p) for p in sorted(libs.glob("*.jar"))],
|
||
"source": str(libs), "cached": True, "_stub": True}
|
||
try:
|
||
r = S.create_modded_server("inst1", version="1.20.1", loader="fabric")
|
||
finally:
|
||
S.fabric_libraries = orig
|
||
assert r["ok"] is True, r
|
||
meta = json.loads((Path(r["instance"]) / "instance.json").read_text())
|
||
assert meta["version"] == "1.20.1", meta
|
||
assert meta["loader"] == "fabric", meta
|
||
assert meta["role"] == "server", meta
|
||
print(" ★ instance.json 记全 version/loader/role")
|
||
|
||
|
||
def test_runtime_dir_is_third_layer():
|
||
"""★ 运行时目录(world/logs/pid)在第三层,不在实例或内容根里。"""
|
||
tmp = _tmp()
|
||
jar = _fake_jar(tmp)
|
||
r = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
|
||
server_jar=str(jar))
|
||
rt = Path(r["runtime"])
|
||
assert rt.parent == S.RUNTIME, rt
|
||
assert rt.name == "inst1", rt
|
||
assert "/store/" not in str(rt) and "/instances/" not in str(rt)
|
||
print(f" ★ 运行时目录在第三层({rt})")
|
||
|
||
|
||
# ---------- ② 未接的加载器必须明说 ----------
|
||
|
||
|
||
def test_unimplemented_loader_says_so():
|
||
"""★★ 没接的加载器(Forge/Quilt)必须**明说**,不能静默降级。
|
||
|
||
静默降级成 vanilla 的后果很严重:服起来了但**mod 全部没加载**,
|
||
而使用者以为装上了 ⇒ 后面所有观察都基于错误前提。
|
||
"""
|
||
tmp = _tmp()
|
||
jar = _fake_jar(tmp)
|
||
for loader in ("forge", "quilt"):
|
||
r = S.create_modded_server(f"inst-{loader}", version="1.21.1",
|
||
loader=loader, server_jar=str(jar))
|
||
assert r["ok"] is False, f"{loader} 不该成功"
|
||
assert loader in r["why"], r
|
||
assert r.get("how_to"), r
|
||
# ★ 不能建出实例目录(否则会留个「看着存在但没配好」的壳)
|
||
assert not (S.INSTANCES / f"inst-{loader}").exists(), \
|
||
f"{loader} 失败时不该留下实例目录"
|
||
print(" ★★ NeoForge/Forge 明确说未接(不静默降级成 vanilla)")
|
||
|
||
|
||
# ---------- ③ 缺文件要指名道姓 ----------
|
||
|
||
|
||
def test_missing_mod_files_are_named():
|
||
"""★ 缺的 mod 要**逐个列出来**(不能吞掉让人以为全装上了)。"""
|
||
tmp = _tmp()
|
||
jar = _fake_jar(tmp)
|
||
r = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
|
||
mod=["/no/such1.jar", "/no/such2.jar"],
|
||
server_jar=str(jar))
|
||
assert r["ok"] is False, r
|
||
assert "such1" in r["why"] and "such2" in r["why"], r["why"]
|
||
assert r.get("installed") == [], r.get("installed")
|
||
print(" ★ 缺 mod 逐个指名(不静默吞)")
|
||
|
||
|
||
def test_neoforge_requires_install_flag():
|
||
"""★ NeoForge 没给 install=True 要**明说**(不能假装装好了)。
|
||
|
||
NeoForge 服务端必须由官方 installer(--installServer)生成,
|
||
没有它就只有一堆 mod jar 而没有可运行的服。
|
||
"""
|
||
tmp = _tmp()
|
||
jar = _fake_jar(tmp)
|
||
r = S.create_modded_server("inst-nf", version="1.21.1", loader="neoforge",
|
||
mod=[str(jar)], install=False)
|
||
assert r["ok"] is False, r
|
||
assert "installer" in r["why"], r
|
||
assert r.get("how_to"), r
|
||
assert not (S.INSTANCES / "inst-nf").exists(), "失败不该留实例目录"
|
||
print(" ★ NeoForge 未 install:明说要跑 installer")
|
||
|
||
|
||
def test_missing_server_jar_for_vanilla_is_reported():
|
||
"""★ vanilla 没给 server.jar 要明说(不能起个残缺服)。"""
|
||
tmp = _tmp()
|
||
r = S.create_modded_server("inst1", version="1.21.1", loader="vanilla")
|
||
assert r["ok"] is False, r
|
||
assert "server_jar" in r["why"], r
|
||
assert r.get("how_to"), r
|
||
print(" ★ vanilla 缺 server.jar 明说")
|
||
|
||
|
||
def test_bad_instance_name_rejected():
|
||
"""★ 实例名要校验(它进路径,含分隔符会越界)。"""
|
||
tmp = _tmp()
|
||
for bad in ("../escape", "a/b", "", "x" * 100, "has space"):
|
||
r = S.create_modded_server(bad, version="1.21.1", loader="vanilla",
|
||
server_jar=str(_fake_jar(tmp)))
|
||
assert r["ok"] is False, f"{bad!r} 应被拒"
|
||
print(" ★ 非法实例名被拒(含路径分隔符)")
|
||
|
||
|
||
# ---------- ④ 启动脚本 ----------
|
||
|
||
|
||
def test_start_script_is_executable_and_self_contained():
|
||
"""★ 脚本要可执行且**自带 cd**(内容根零写入 ⇒ 不能依赖调用者 cwd)。"""
|
||
tmp = _tmp()
|
||
jar = _fake_jar(tmp)
|
||
r = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
|
||
server_jar=str(jar))
|
||
script = Path(r["start_script"])
|
||
assert script.exists(), script
|
||
assert script.stat().st_mode & stat.S_IXUSR, "应可执行"
|
||
body = script.read_text(encoding="utf-8")
|
||
assert 'cd "$(dirname "$0")"' in body, "应自带 cd 到实例目录"
|
||
assert "server.jar" in body
|
||
print(" ★ 启动脚本可执行且自带 cd")
|
||
|
||
|
||
def test_repeated_creation_is_idempotent_report():
|
||
"""★ 重复创建同一实例要**如实报告**(不假装全新,也不默默覆盖 mod)。"""
|
||
tmp = _tmp()
|
||
jar = _fake_jar(tmp)
|
||
a = _fake_jar(tmp, "modA.jar")
|
||
r1 = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
|
||
mod=[str(a)], server_jar=str(jar))
|
||
r2 = S.create_modded_server("inst1", version="1.21.1", loader="vanilla",
|
||
mod=[str(a)], server_jar=str(jar))
|
||
assert r1["ok"] and r2["ok"]
|
||
# 两次都成功(幂等),且 mod 没被重复堆积
|
||
mods = sorted(p.name for p in (Path(r2["instance"]) / "mods").iterdir())
|
||
assert mods == ["modA.jar"], mods
|
||
print(" ★ 重复创建幂等(mod 不堆积)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
for fn in [test_mods_land_in_instance_not_content_root,
|
||
test_two_instances_share_content_root_different_mods,
|
||
test_instance_json_records_version_and_loader,
|
||
test_runtime_dir_is_third_layer,
|
||
test_unimplemented_loader_says_so,
|
||
test_missing_mod_files_are_named,
|
||
test_neoforge_requires_install_flag,
|
||
test_missing_server_jar_for_vanilla_is_reported,
|
||
test_bad_instance_name_rejected,
|
||
test_start_script_is_executable_and_self_contained,
|
||
test_repeated_creation_is_idempotent_report]:
|
||
fn()
|
||
print("\n加载器服务端:全部通过")
|