mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-12 20:10:05 +08:00
fixed # 修复手机端地图滑动异常的问题
fixed # 重构更新框架,支持选择是否下载梦三模式 fixed # 补充部分意图的资源图片与逻辑
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# 更新清单与可选模块
|
||||
|
||||
## 梦三模式
|
||||
|
||||
梦三仍与主包保存在同一仓库,但作为独立下载模块发布。基础清单
|
||||
`dist/manifest.json` 不包含 `mengsan/` 文件;模块清单
|
||||
`dist/modules/mengsan.json` 单独记录梦三文件、大小和 SHA-256。
|
||||
|
||||
用户通过扩展菜单的“梦三模式(按需安装/卸载)”下载或卸载。主包版本
|
||||
更新只检查主包文件,以及本地已安装模块的文件;“查漏补缺”会检查主包
|
||||
资源和已安装模块,不会自动下载未安装的梦三模式。
|
||||
|
||||
## 发布流程
|
||||
|
||||
1. 在 `tools/manifest_config.json` 的 `core` 中维护主包必须文件。
|
||||
2. 在 `modules` 中维护可选模块的目录、入口和显示名。
|
||||
3. 发布工作流为每个模块生成独立清单,并将清单提交到发布标签。
|
||||
4. 完整 ZIP 安装包仍包含所有模块,供离线或整包安装使用。
|
||||
|
||||
客户端从标签的 raw 文件路径读取清单。因此 Gitea 发布必须使用已经包含
|
||||
`dist/manifest.json` 和 `dist/modules/*.json` 的同一标签;当前 Gitea 工作流
|
||||
会生成并上传附件,但不会改写触发它的标签。
|
||||
|
||||
发布时生成器会把 `tools/legacy/mengsan-files.json`、上一次完整清单和当前模块
|
||||
文件合并,随新标签发布迁移路径清单。它用于接管旧安装中的梦三文件并清理
|
||||
不再属于新版本的文件。该文件由迁移时仓库内的梦三路径生成,作为冻结的旧版
|
||||
所有权记录,兼容旧更新器已经推进主包版本号、但尚未更新梦三模块的用户。
|
||||
|
||||
## 旧文件与目录
|
||||
|
||||
升级时比较旧版和新版的文件路径清单,删除旧清单有、新清单没有的模块文件;
|
||||
新增或损坏文件会按 SHA-256 下载和修复。模块卸载只删除模块清单声明的文件。
|
||||
|
||||
不根据“目录不在新清单中”递归删除目录,避免误删用户自行放入目录的文件。
|
||||
确需删除整个旧目录时,先确认其不包含用户数据,再通过主包清单的
|
||||
`removeDirectories` 明确声明。
|
||||
|
||||
新增模块时,清单生成器可通过配置建立独立清单;客户端的安装菜单、启动
|
||||
加载和卸载入口仍需为该模块添加对应管理逻辑。
|
||||
+200
-49
@@ -68,15 +68,36 @@ def load_config(root: Path, config_path: str) -> dict:
|
||||
"core": [],
|
||||
"remove": [],
|
||||
"removeDirectories": [],
|
||||
"modules": {},
|
||||
}
|
||||
|
||||
with path.open("r", encoding="utf-8") as file:
|
||||
config = json.load(file)
|
||||
|
||||
modules = {}
|
||||
for name, item in config.get("modules", {}).items():
|
||||
legacy_files = item.get("legacy_files", [])
|
||||
legacy_source = item.get("legacy_files_from")
|
||||
if legacy_source:
|
||||
legacy_path = Path(legacy_source)
|
||||
if not legacy_path.is_absolute():
|
||||
legacy_path = root / legacy_path
|
||||
with legacy_path.open("r", encoding="utf-8") as file:
|
||||
legacy_files = json.load(file)
|
||||
if not isinstance(legacy_files, list):
|
||||
raise ValueError(f"legacy file list must be an array: {name}")
|
||||
modules[name] = {
|
||||
"root": normalize_path(item["root"]),
|
||||
"entry": normalize_path(item["entry"]),
|
||||
"title": item.get("title", name),
|
||||
"legacy_files": [normalize_path(path) for path in legacy_files],
|
||||
}
|
||||
|
||||
return {
|
||||
"core": [normalize_path(item) for item in config.get("core", [])],
|
||||
"remove": [normalize_path(item) for item in config.get("remove", [])],
|
||||
"removeDirectories": [normalize_path(item) for item in config.get("removeDirectories", [])],
|
||||
"modules": modules,
|
||||
}
|
||||
|
||||
|
||||
@@ -117,67 +138,168 @@ def set_nested_file(tree: dict, relative_path: str, info: dict) -> None:
|
||||
}
|
||||
|
||||
|
||||
def build_manifest(root: Path, version: str, config: dict) -> dict:
|
||||
flat_files = {}
|
||||
tree = {
|
||||
"type": "directory",
|
||||
"name": root.name,
|
||||
"children": {},
|
||||
}
|
||||
core_files = config["core"]
|
||||
core_file_set = set(core_files)
|
||||
remove_files = config["remove"]
|
||||
remove_directories = config["removeDirectories"]
|
||||
validate_config_files(root, core_files, "core")
|
||||
validate_relative_paths(remove_files, "remove")
|
||||
validate_relative_paths(remove_directories, "removeDirectories")
|
||||
def build_manifest(root: Path, version: str, config: dict,
|
||||
previous_manifest=None) -> tuple[dict, dict, dict]:
|
||||
module_configs = config.get("modules", {})
|
||||
module_roots = {name: item["root"] for name, item in module_configs.items()}
|
||||
validate_relative_paths(list(module_roots.values()), "module roots")
|
||||
if len(set(module_roots.values())) != len(module_roots):
|
||||
raise ValueError("module roots must be unique")
|
||||
for name, item in module_configs.items():
|
||||
for other_name, other_root in module_roots.items():
|
||||
if name != other_name and (
|
||||
item["root"] == other_root
|
||||
or item["root"].startswith(other_root + "/")
|
||||
or other_root.startswith(item["root"] + "/")
|
||||
):
|
||||
raise ValueError("module roots cannot overlap")
|
||||
|
||||
def owned_by_module(path: str) -> bool:
|
||||
return any(path == module_root or path.startswith(module_root + "/")
|
||||
for module_root in module_roots.values())
|
||||
|
||||
config["remove"] = [file for file in config["remove"]
|
||||
if not owned_by_module(file)]
|
||||
config["removeDirectories"] = [
|
||||
directory for directory in config["removeDirectories"]
|
||||
if not any(directory == module_root
|
||||
or directory.startswith(module_root + "/")
|
||||
or module_root.startswith(directory + "/")
|
||||
for module_root in module_roots.values())
|
||||
]
|
||||
|
||||
all_files = {}
|
||||
for path in scan_files(root):
|
||||
relative_path = str(path.relative_to(root)).replace("\\", "/")
|
||||
info = {
|
||||
all_files[relative_path] = {
|
||||
"size": path.stat().st_size,
|
||||
"sha256": sha256_file(path),
|
||||
}
|
||||
|
||||
flat_files[relative_path] = info
|
||||
set_nested_file(tree["children"], relative_path, info)
|
||||
def make_manifest(file_map: dict, core_files: list[str], removals: list[str],
|
||||
remove_directories: list[str], module_id: str = "") -> dict:
|
||||
tree = {
|
||||
"type": "directory",
|
||||
"name": root.name,
|
||||
"children": {},
|
||||
}
|
||||
for relative_path, info in file_map.items():
|
||||
set_nested_file(tree["children"], relative_path, info)
|
||||
|
||||
remove_file_conflicts = [file for file in remove_files if file in flat_files]
|
||||
if remove_file_conflicts:
|
||||
raise ValueError(f"remove still contains current files: {', '.join(remove_file_conflicts)}")
|
||||
core_file_set = set(core_files)
|
||||
validate_config_files(root, core_files, "core")
|
||||
missing_core = [file for file in core_files if file not in file_map]
|
||||
if missing_core:
|
||||
raise ValueError(
|
||||
"core contains files outside this manifest: "
|
||||
+ ", ".join(missing_core)
|
||||
)
|
||||
validate_relative_paths(removals, "remove")
|
||||
validate_relative_paths(remove_directories, "removeDirectories")
|
||||
conflicts = [file for file in removals if file in file_map]
|
||||
if conflicts:
|
||||
raise ValueError(f"remove still contains current files: {', '.join(conflicts)}")
|
||||
directory_conflicts = [
|
||||
directory for directory in remove_directories
|
||||
if any(file == directory or file.startswith(directory + "/")
|
||||
for file in file_map)
|
||||
]
|
||||
if directory_conflicts:
|
||||
raise ValueError(
|
||||
"removeDirectories still contain current files: "
|
||||
+ ", ".join(directory_conflicts)
|
||||
)
|
||||
|
||||
remove_directory_conflicts = [
|
||||
directory for directory in remove_directories
|
||||
if any(file == directory or file.startswith(f"{directory}/") for file in flat_files)
|
||||
]
|
||||
if remove_directory_conflicts:
|
||||
raise ValueError(
|
||||
"removeDirectories still contain current files: "
|
||||
+ ", ".join(remove_directory_conflicts)
|
||||
)
|
||||
folder_digest = hashlib.sha256()
|
||||
for relative_path in sorted(file_map):
|
||||
info = file_map[relative_path]
|
||||
folder_digest.update(relative_path.encode("utf-8"))
|
||||
folder_digest.update(str(info["size"]).encode("ascii"))
|
||||
folder_digest.update(info["sha256"].encode("ascii"))
|
||||
|
||||
folder_digest = hashlib.sha256()
|
||||
for relative_path in sorted(flat_files):
|
||||
info = flat_files[relative_path]
|
||||
folder_digest.update(relative_path.encode("utf-8"))
|
||||
folder_digest.update(str(info["size"]).encode("ascii"))
|
||||
folder_digest.update(info["sha256"].encode("ascii"))
|
||||
result = {
|
||||
"version": version,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"algorithm": "sha256",
|
||||
"update_channels": True,
|
||||
"file_count": len(file_map),
|
||||
"size_bytes": sum(info["size"] for info in file_map.values()),
|
||||
"folder_sha256": folder_digest.hexdigest(),
|
||||
"core": core_files,
|
||||
"assets": [file for file in sorted(file_map) if file not in core_file_set],
|
||||
"remove": removals,
|
||||
"removeDirectories": remove_directories,
|
||||
"files": file_map,
|
||||
"tree": tree,
|
||||
}
|
||||
if module_id:
|
||||
result["module_id"] = module_id
|
||||
return result
|
||||
|
||||
return {
|
||||
"version": version,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"algorithm": "sha256",
|
||||
"update_channels": True,
|
||||
"file_count": len(flat_files),
|
||||
"folder_sha256": folder_digest.hexdigest(),
|
||||
"core": core_files,
|
||||
"assets": [file for file in sorted(flat_files) if file not in core_file_set],
|
||||
"remove": remove_files,
|
||||
"removeDirectories": remove_directories,
|
||||
"files": flat_files,
|
||||
"tree": tree,
|
||||
module_files = {name: {} for name in module_configs}
|
||||
base_files = {}
|
||||
for path, info in all_files.items():
|
||||
owner = next((name for name, module_root in module_roots.items()
|
||||
if path == module_root or path.startswith(module_root + "/")), None)
|
||||
if owner:
|
||||
module_files[owner][path] = info
|
||||
else:
|
||||
base_files[path] = info
|
||||
|
||||
base_manifest = make_manifest(
|
||||
base_files, config["core"], config["remove"], config["removeDirectories"]
|
||||
)
|
||||
base_manifest["modules"] = {
|
||||
name: {
|
||||
"title": item["title"],
|
||||
"entry": item["entry"],
|
||||
"manifest": f"dist/modules/{name}.json",
|
||||
"file_count": len(module_files[name]),
|
||||
"size_bytes": sum(info["size"] for info in module_files[name].values()),
|
||||
}
|
||||
for name, item in module_configs.items()
|
||||
}
|
||||
|
||||
manifests = {}
|
||||
legacy_manifests = {}
|
||||
for name, item in module_configs.items():
|
||||
files = module_files[name]
|
||||
if item["entry"] not in files:
|
||||
raise FileNotFoundError(f"module entry is missing: {item['entry']}")
|
||||
manifests[name] = make_manifest(
|
||||
files, [item["entry"]], [], [], module_id=name
|
||||
)
|
||||
manifests[name]["assets"] = [
|
||||
file for file in sorted(files) if file != item["entry"]
|
||||
]
|
||||
if not item["entry"].startswith(item["root"] + "/"):
|
||||
raise ValueError(f"module entry is outside its root: {item['entry']}")
|
||||
previous_files = previous_manifest.get("files", {}) \
|
||||
if isinstance(previous_manifest, dict) else {}
|
||||
legacy_files = set(item["legacy_files"])
|
||||
legacy_files.update(module_files[name])
|
||||
legacy_files.update(
|
||||
file for file in previous_files
|
||||
if isinstance(file, str) and file.startswith(item["root"] + "/")
|
||||
)
|
||||
legacy_files = sorted(legacy_files)
|
||||
if legacy_files:
|
||||
validate_relative_paths(list(legacy_files), "legacy module files")
|
||||
if any(not file.startswith(item["root"] + "/")
|
||||
for file in legacy_files):
|
||||
raise ValueError(f"legacy files escape module root: {name}")
|
||||
legacy_manifests[name] = {
|
||||
"version": version,
|
||||
"module_id": name,
|
||||
"snapshot": "legacy",
|
||||
"files": legacy_files,
|
||||
}
|
||||
base_manifest["modules"][name]["legacy_manifest"] = (
|
||||
f"dist/modules/{name}.legacy.json"
|
||||
)
|
||||
|
||||
return base_manifest, manifests, legacy_manifests
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Generate shuying manifest.json")
|
||||
@@ -194,10 +316,37 @@ def main() -> None:
|
||||
raise ValueError("Version cannot be empty")
|
||||
|
||||
output = (root / args.out).resolve()
|
||||
previous_manifest = None
|
||||
if output.is_file():
|
||||
with output.open("r", encoding="utf-8") as file:
|
||||
previous_manifest = json.load(file)
|
||||
config = preserve_removals(load_config(root, args.config), output)
|
||||
manifest = build_manifest(root, version, config)
|
||||
manifest, modules, legacy_modules = build_manifest(
|
||||
root, version, config, previous_manifest
|
||||
)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
module_output = output.parent / "modules"
|
||||
for name, module_manifest in modules.items():
|
||||
module_path = module_output / f"{name}.json"
|
||||
module_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
module_path.write_text(
|
||||
json.dumps(module_manifest, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
for name, module_manifest in legacy_modules.items():
|
||||
module_path = module_output / f"{name}.legacy.json"
|
||||
module_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
module_path.write_text(
|
||||
json.dumps(module_manifest, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
if module_output.is_dir():
|
||||
expected = {f"{name}.json" for name in modules}
|
||||
expected.update(f"{name}.legacy.json" for name in legacy_modules)
|
||||
for stale in module_output.glob("*.json"):
|
||||
if stale.name not in expected:
|
||||
stale.unlink()
|
||||
|
||||
print(f"version: {version}")
|
||||
print(f"file_count: {manifest['file_count']}")
|
||||
@@ -207,6 +356,8 @@ def main() -> None:
|
||||
print(f"remove_directory_count: {len(manifest['removeDirectories'])}")
|
||||
print(f"folder_sha256: {manifest['folder_sha256']}")
|
||||
print(f"generated: {output}")
|
||||
for name, module_manifest in modules.items():
|
||||
print(f"module_{name}_file_count: {module_manifest['file_count']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
[
|
||||
"mengsan/assets/flyconid-portrait.png",
|
||||
"mengsan/assets/intent/Intent_attack_1.webp",
|
||||
"mengsan/assets/intent/Intent_attack_2.webp",
|
||||
"mengsan/assets/intent/Intent_attack_3.webp",
|
||||
"mengsan/assets/intent/Intent_attack_4.webp",
|
||||
"mengsan/assets/intent/Intent_attack_5.webp",
|
||||
"mengsan/assets/intent/Intent_debuff.webp",
|
||||
"mengsan/assets/sushuang-zhangshiping.png",
|
||||
"mengsan/config.js",
|
||||
"mengsan/content/acts/act1/index.js",
|
||||
"mengsan/content/acts/act1/map.js",
|
||||
"mengsan/content/acts/act1/rewards.js",
|
||||
"mengsan/content/acts/act1/stories/forest-ambush.js",
|
||||
"mengsan/content/acts/act1/stories/liubei-opening.js",
|
||||
"mengsan/content/acts/act2/index.js",
|
||||
"mengsan/content/acts/act2/map.js",
|
||||
"mengsan/content/acts/act3/index.js",
|
||||
"mengsan/content/acts/act3/map.js",
|
||||
"mengsan/content/affixes.js",
|
||||
"mengsan/content/card-definitions.js",
|
||||
"mengsan/content/card-library.js",
|
||||
"mengsan/content/mode-cards.js",
|
||||
"mengsan/content/registry.js",
|
||||
"mengsan/content/scenario-characters.js",
|
||||
"mengsan/content/shared/index.js",
|
||||
"mengsan/content/shared/rewards.js",
|
||||
"mengsan/docs/battle-config.md",
|
||||
"mengsan/docs/combat-rules.md",
|
||||
"mengsan/docs/liubei-opening.md",
|
||||
"mengsan/map/index.js",
|
||||
"mengsan/map/renderer.js",
|
||||
"mengsan/mode.js",
|
||||
"mengsan/register.js",
|
||||
"mengsan/runtime/battle-director.js",
|
||||
"mengsan/runtime/battle-flow.js",
|
||||
"mengsan/runtime/battle-loop.js",
|
||||
"mengsan/runtime/battle-resources.js",
|
||||
"mengsan/runtime/battle-session.js",
|
||||
"mengsan/runtime/battle-settlement.js",
|
||||
"mengsan/runtime/card-data.js",
|
||||
"mengsan/runtime/card-library.js",
|
||||
"mengsan/runtime/combat-rules.js",
|
||||
"mengsan/runtime/dialogue-choice.js",
|
||||
"mengsan/runtime/dialogue.js",
|
||||
"mengsan/runtime/engine-session.js",
|
||||
"mengsan/runtime/flow-ui.js",
|
||||
"mengsan/runtime/flyconid-intent.js",
|
||||
"mengsan/runtime/gm-manager.js",
|
||||
"mengsan/runtime/intent-display.js",
|
||||
"mengsan/runtime/mode-storage.js",
|
||||
"mengsan/runtime/monster.js",
|
||||
"mengsan/runtime/navigation.js",
|
||||
"mengsan/runtime/personal-piles.js",
|
||||
"mengsan/runtime/reward-ui.js",
|
||||
"mengsan/runtime/reward.js",
|
||||
"mengsan/runtime/skill-teardown.js",
|
||||
"mengsan/runtime/story.js",
|
||||
"mengsan/state.js",
|
||||
"mengsan/style.css"
|
||||
]
|
||||
@@ -4,10 +4,6 @@
|
||||
"menu.js",
|
||||
"function.js",
|
||||
"update.js",
|
||||
"mengsan/mode.js",
|
||||
"mengsan/config.js",
|
||||
"mengsan/state.js",
|
||||
"mengsan/style.css",
|
||||
"tianshu/extension.js",
|
||||
"tianshu/config.js",
|
||||
"pve/boss/index.js",
|
||||
@@ -79,5 +75,13 @@
|
||||
"pve/boss/sets/tianshu/voices.js"
|
||||
],
|
||||
"remove": [],
|
||||
"removeDirectories": []
|
||||
"removeDirectories": [],
|
||||
"modules": {
|
||||
"mengsan": {
|
||||
"root": "mengsan",
|
||||
"entry": "mengsan/register.js",
|
||||
"title": "梦三模式",
|
||||
"legacy_files_from": "tools/legacy/mengsan-files.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user