77 lines
2.9 KiB
GDScript
77 lines
2.9 KiB
GDScript
## 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()))
|