forked from xiangwang25/whale-town-front-v2
refactor: harden client runtime and exports
This commit is contained in:
21
scripts/audit_asset_size.sh
Executable file
21
scripts/audit_asset_size.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Keep single source assets below 20 MiB by default. Override in CI when a
|
||||
# stricter product budget is available.
|
||||
limit_bytes="${ASSET_LIMIT_BYTES:-20971520}"
|
||||
status=0
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
size=$(wc -c < "$file")
|
||||
if (( size > limit_bytes )); then
|
||||
printf '%s\t%s\n' "$size" "$file"
|
||||
status=1
|
||||
fi
|
||||
done < <(git ls-files -z -- 'assets/**')
|
||||
|
||||
if (( status != 0 )); then
|
||||
printf '发现超过 %s 字节的受版本控制资源;请压缩、外置或使用 Git LFS。\n' "$limit_bytes" >&2
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
72
scripts/externalize_work_zone_tileset.gd
Normal file
72
scripts/externalize_work_zone_tileset.gd
Normal file
@@ -0,0 +1,72 @@
|
||||
extends SceneTree
|
||||
|
||||
const TILESET_PATH: String = "res://assets/maps/work_zone/v13/tilesets/work_zone_road_tileset.tres"
|
||||
const TEXTURE_DIR: String = "res://assets/maps/work_zone/v13/tilesets"
|
||||
|
||||
func _init() -> void:
|
||||
var tileset := load(TILESET_PATH) as TileSet
|
||||
if tileset == null:
|
||||
push_error("无法加载 TileSet: %s" % TILESET_PATH)
|
||||
quit(1)
|
||||
return
|
||||
var candidates := _load_candidates()
|
||||
var replaced := 0
|
||||
for source_index in tileset.get_source_count():
|
||||
var source_id := tileset.get_source_id(source_index)
|
||||
var atlas_source := tileset.get_source(source_id) as TileSetAtlasSource
|
||||
if atlas_source == null or atlas_source.texture == null:
|
||||
continue
|
||||
var source_hash := _texture_hash(atlas_source.texture)
|
||||
var replacement: Texture2D = candidates.get(source_hash)
|
||||
if replacement == null:
|
||||
_write_audit_image(source_id, atlas_source.texture)
|
||||
push_error("未找到匹配的外置纹理,source=%d hash=%s" % [source_id, source_hash])
|
||||
quit(2)
|
||||
return
|
||||
atlas_source.texture = replacement
|
||||
replaced += 1
|
||||
var error := ResourceSaver.save(tileset, TILESET_PATH)
|
||||
if error != OK:
|
||||
push_error("保存 TileSet 失败: %s" % error_string(error))
|
||||
quit(3)
|
||||
return
|
||||
print("已外置 %d 个 TileSet 纹理源" % replaced)
|
||||
quit()
|
||||
|
||||
func _load_candidates() -> Dictionary:
|
||||
var candidates: Dictionary = {}
|
||||
for file_name in DirAccess.get_files_at(TEXTURE_DIR):
|
||||
if not file_name.ends_with(".png"):
|
||||
continue
|
||||
var path := "%s/%s" % [TEXTURE_DIR, file_name]
|
||||
var texture := load(path) as Texture2D
|
||||
if texture != null:
|
||||
candidates[_bytes_hash(FileAccess.get_file_as_bytes(path))] = texture
|
||||
return candidates
|
||||
|
||||
func _texture_hash(texture: Texture2D) -> String:
|
||||
var image := texture.get_image()
|
||||
if image == null:
|
||||
return ""
|
||||
return _bytes_hash(image.save_png_to_buffer())
|
||||
|
||||
func _bytes_hash(bytes: PackedByteArray) -> String:
|
||||
var context := HashingContext.new()
|
||||
var start_error := context.start(HashingContext.HASH_SHA256)
|
||||
if start_error != OK:
|
||||
return ""
|
||||
var update_error := context.update(bytes)
|
||||
if update_error != OK:
|
||||
return ""
|
||||
return context.finish().hex_encode()
|
||||
|
||||
func _write_audit_image(source_id: int, texture: Texture2D) -> void:
|
||||
var directory := ProjectSettings.globalize_path("res://build/tileset-audit")
|
||||
DirAccess.make_dir_recursive_absolute(directory)
|
||||
var image := texture.get_image()
|
||||
if image == null:
|
||||
return
|
||||
var path := "%s/source_%d.png" % [directory, source_id]
|
||||
var error := image.save_png(path)
|
||||
if error == OK:
|
||||
print("已写出待核对纹理: %s (%dx%d)" % [path, image.get_width(), image.get_height()])
|
||||
1
scripts/externalize_work_zone_tileset.gd.uid
Normal file
1
scripts/externalize_work_zone_tileset.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cgcum8jwl147u
|
||||
40
scripts/test_secure_session_store.gd
Normal file
40
scripts/test_secure_session_store.gd
Normal file
@@ -0,0 +1,40 @@
|
||||
extends SceneTree
|
||||
|
||||
const SecureSessionStore = preload("res://_Core/security/SecureSessionStore.gd")
|
||||
const TEST_PATH: String = "user://whaletown-secure-session-test.json"
|
||||
const TEST_TOKEN: String = "refresh-token-test-value"
|
||||
|
||||
func _init() -> void:
|
||||
var store: RefCounted = SecureSessionStore.new()
|
||||
store.call("clear", TEST_PATH)
|
||||
var saved := bool(store.call("save_refresh_token", TEST_PATH, TEST_TOKEN))
|
||||
if not saved:
|
||||
print("安全持久化在当前平台不可用,已按仅内存策略跳过")
|
||||
quit()
|
||||
return
|
||||
var loaded := str(store.call("load_refresh_token", TEST_PATH))
|
||||
if loaded != TEST_TOKEN:
|
||||
push_error("安全会话存储往返测试失败")
|
||||
quit(1)
|
||||
return
|
||||
var payload_variant: Variant = JSON.parse_string(FileAccess.get_file_as_string(TEST_PATH))
|
||||
if not (payload_variant is Dictionary):
|
||||
push_error("安全会话存储测试载荷解析失败")
|
||||
quit(1)
|
||||
return
|
||||
var payload: Dictionary = payload_variant
|
||||
payload["mac"] = Marshalls.raw_to_base64(PackedByteArray([1, 2, 3, 4]))
|
||||
var tampered_file := FileAccess.open(TEST_PATH, FileAccess.WRITE)
|
||||
if tampered_file == null:
|
||||
push_error("安全会话存储篡改测试无法写入")
|
||||
quit(1)
|
||||
return
|
||||
tampered_file.store_string(JSON.stringify(payload))
|
||||
tampered_file.close()
|
||||
var tampered_token := str(store.call("load_refresh_token", TEST_PATH))
|
||||
if not tampered_token.is_empty() or FileAccess.file_exists(TEST_PATH):
|
||||
push_error("安全会话存储未拒绝被篡改载荷")
|
||||
quit(1)
|
||||
return
|
||||
print("安全会话存储往返与篡改检测测试通过")
|
||||
quit()
|
||||
1
scripts/test_secure_session_store.gd.uid
Normal file
1
scripts/test_secure_session_store.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ct3bimrlbknt3
|
||||
Reference in New Issue
Block a user