Files
spellforge/scripts/autoloads/endless_records.gd
T
2026-07-20 10:56:52 +08:00

77 lines
2.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## EndlessRecords — 本地排行榜(Autoload: EndlessRecords
## 权威来源:certification_checklist.md ST-40/41HMAC-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()))