## EndlessRecords — 本地排行榜(Autoload: EndlessRecords) ## 权威来源:certification_checklist.md ST-40/41(HMAC-SHA256 防篡改)、P6-N19 评分公式 ## 排名:到达波次为主、同波次按用时升序;在线分 = wave*100000 + (86400 - elapsed_sec) extends Node const RECORDS_PATH: String = "user://endless_records.json" const MAX_RECORDS: int = 50 ## HMAC 密钥(不入存档;仅防普通玩家手改,无法对抗逆向,见 ST-40) ## PackedByteArray(...) 非常量表达式,故用 var 在加载时初始化 var _HMAC_KEY: PackedByteArray = PackedByteArray([ 0xA3, 0x7F, 0x2C, 0x91, 0x4E, 0xD8, 0x12, 0x6B, 0xF0, 0x39, 0x5A, 0xCC, 0x77, 0x1D, 0x8E, 0x44, 0x22, 0xB9, 0x60, 0x3F, 0xE1, 0x05, 0x9C, 0x7A, 0x53, 0xD4, 0x08, 0x6F, 0xAB, 0x31, 0xC7, 0x90, ]) ## P6-N19:单整数双维度评分(波次高优先;同波次剩余秒数高优先) static func compute_score(wave: int, elapsed_sec: int) -> int: return wave * 100000 + (86400 - clampi(elapsed_sec, 0, 86400)) func _sign(data: String) -> String: var crypto := Crypto.new() var hmac := crypto.hmac_digest(HashingContext.HASH_SHA256, _HMAC_KEY, data.to_utf8_buffer()) return hmac.hex_encode() ## 追加一条记录并持久化;返回排序后的全部记录 func add_record(wave: int, elapsed_sec: int, kills: int) -> Array: var records: Array = load_records() records.append({ "wave": wave, "elapsed": elapsed_sec, "kills": kills, "score": compute_score(wave, elapsed_sec), }) records = _sort_records(records) if records.size() > MAX_RECORDS: records.resize(MAX_RECORDS) save_records(records) return records ## data 以 JSON 字符串存储并对该精确字符串签名(避免 JSON int↔float 往返破坏签名) func save_records(records: Array) -> void: var payload: String = JSON.stringify(records) var signed: Dictionary = {"data": payload, "sig": _sign(payload)} var f := FileAccess.open(RECORDS_PATH, FileAccess.WRITE) if f: f.store_string(JSON.stringify(signed)) f.close() func load_records() -> Array: if not FileAccess.file_exists(RECORDS_PATH): return [] var raw = JSON.parse_string(FileAccess.get_file_as_string(RECORDS_PATH)) if not (raw is Dictionary): return [] var payload: String = String(raw.get("data", "")) if _sign(payload) != String(raw.get("sig", "")): push_warning("EndlessRecords: 签名验证失败,分数不可信,拒绝载入") # ST-41 return [] var data = JSON.parse_string(payload) return data if data is Array else [] ## 波次降序、同波次用时升序(P6-N19) func _sort_records(records: Array) -> Array: records.sort_custom(func(a, b): var wa: int = int(a.get("wave", 0)) var wb: int = int(b.get("wave", 0)) if wa != wb: return wa > wb return int(a.get("elapsed", 99999)) < int(b.get("elapsed", 99999))) return records func get_top(n: int = 10) -> Array: var r: Array = load_records() return r.slice(0, min(n, r.size()))