feat(tools): 数值验证工具批——自动化评判三段采集(直入口基线带判定+零强化止步归因+营地循环爬升战役)
- 冒险摘要增败退明细 defeat{level_index,node_id,node_type}(仅失败局附加,消费方零感知)
- level_autoplay 冒险驱动增 hero_override 尾参透传(永久强化 flat 注入)+末场战斗敌方名采集
- 新增采集端 smoke/verifier_batch.gd(VJSON 行契约,隔离档真实元进度账本,绝不碰 user://save.json)
- 新增判定端 tools/balance_verifier.py(带判定/止步归因/滑窗检查点判定/报告落 production/qa)
- 冒烟 552 项 0 失败(新增败退明细断言 2 项)
This commit is contained in:
1 parent
ac16796f29
commit
e8d8d6051e
7 files changed
+723
-7
No files matched your search
@@ -54,6 +54,7 @@ var master_seed := 0 # 本局主种子(摘要第五件事
|
||||
var summary := {} # 最近一次结算的冒险摘要(idle 后保留——重开才新生成)
|
||||
var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离)
|
||||
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
|
||||
var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面)
|
||||
var _override := [0, 0] # TunePanel 百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用)
|
||||
var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
|
||||
|
||||
@@ -165,6 +166,7 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
if state != "running":
|
||||
push_error("[冒险结构] 重复关卡结束宣布拒绝——state=%s(账本与摘要零变化)" % state)
|
||||
return
|
||||
_last_level_payload = payload.duplicate(true)
|
||||
var result := String(payload.get("result", ""))
|
||||
if result == "defeat":
|
||||
ledger["hero_hp"] = 0 # 倒下记 0(生命账本战后回写口径)
|
||||
@@ -204,6 +206,14 @@ func _settle(result: String) -> void:
|
||||
"keystone_total": keystone,
|
||||
"master_seed": master_seed,
|
||||
}
|
||||
# 败退明细(数值验证工具取数面 2026-09-22——tools/balance_verifier.py 止步节点分布数据源;
|
||||
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
|
||||
if result == "failed" and not _last_level_payload.is_empty():
|
||||
summary["defeat"] = {
|
||||
"level_index": level_index,
|
||||
"node_id": int(_last_level_payload.get("node_id", -1)),
|
||||
"node_type": String(_last_level_payload.get("node_type", "")),
|
||||
}
|
||||
if _runner != null:
|
||||
_runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现)
|
||||
state = "settling"
|
||||
|
||||
@@ -35,10 +35,10 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
|
||||
var st := {
|
||||
"result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0,
|
||||
"rest_healed": 0, "hp_before_rest": -1,
|
||||
"defeat_node_index": -1, "defeat_node_type": "",
|
||||
"defeat_node_index": -1, "defeat_node_id": -1, "defeat_node_type": "",
|
||||
"boss_depth": -1, "hp_at_boss": -1, "rows": [], "timeout": false,
|
||||
"log_path": "", "policy": policy, "refreshes": 0, "mine_hits": 0,
|
||||
"visited_ids": [], "choices": 0,
|
||||
"visited_ids": [], "choices": 0, "last_battle_enemies": [],
|
||||
}
|
||||
runner.step_row.connect(_on_step_row.bind(runner, st, collect_rows))
|
||||
runner.node_entered.connect(_on_node_entered.bind(runner, st, logger))
|
||||
@@ -86,24 +86,29 @@ static func _drive_level(runner, st: Dictionary, logger, policy: String, route:
|
||||
# (start_adventure 唯一 new_adventure 调用点)、大奖励立即确认、冒险终局置 result/summary。
|
||||
# 返回结构 = run_level 同族 + summary / wallet_events / encounter_max_hp_seq / reward_events /
|
||||
# visited_ids(全冒险节点行进序列——跨关累计,非末关 runner 的 visited 随实例更替,此处为唯一全量面)。
|
||||
# 2026-09-22 数值验证工具批:增 hero_override 尾参(永久强化 flat 注入透传——缺省 {} 与既有调用方
|
||||
# 逐位一致零行为变更)+ run 行携带该参数(分析侧按元进度档切分)+ 败退敌人名采集
|
||||
# (last_battle_enemies——末场战斗敌方显示名序列,失败局即死亡遭遇)。
|
||||
static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_rows: bool = false,
|
||||
logger = null, policy: String = "normal", route: String = "min") -> Dictionary:
|
||||
logger = null, policy: String = "normal", route: String = "min",
|
||||
hero_override: Dictionary = {}) -> Dictionary:
|
||||
var own_session := logger == null
|
||||
if own_session:
|
||||
logger = load("res://presentation/playtest_logger.gd").new()
|
||||
logger.start_session(seed_val)
|
||||
logger.ap_per_round = int(db.action_economy.ap_per_round)
|
||||
logger.log_run({"seed": seed_val, "adventure": true, "atk_pct": atk_pct, "hp_pct": hp_pct, "policy": policy, "route": route})
|
||||
logger.log_run({"seed": seed_val, "adventure": true, "atk_pct": atk_pct, "hp_pct": hp_pct,
|
||||
"policy": policy, "route": route, "hero_override": hero_override})
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_val)
|
||||
var adr = load("res://core/adventure_runner.gd").new(db, rng)
|
||||
var st := {
|
||||
"result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0,
|
||||
"rest_healed": 0, "hp_before_rest": -1,
|
||||
"defeat_node_index": -1, "defeat_node_type": "",
|
||||
"defeat_node_index": -1, "defeat_node_id": -1, "defeat_node_type": "",
|
||||
"boss_depth": -1, "hp_at_boss": -1, "rows": [], "timeout": false,
|
||||
"log_path": "", "policy": policy, "refreshes": 0, "mine_hits": 0,
|
||||
"visited_ids": [], "choices": 0,
|
||||
"visited_ids": [], "choices": 0, "last_battle_enemies": [],
|
||||
"summary": {}, "wallet_events": [], "encounter_max_hp_seq": [], "reward_events": [],
|
||||
"shop_gp_seq": [], # 商店房到达余额序(逐关 1 次——GP 跨关延续验收取数面)
|
||||
}
|
||||
@@ -113,7 +118,7 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
adr.big_reward_paid.connect(_on_adventure_reward.bind(st, adr))
|
||||
adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger))
|
||||
adr.set_hero_override(atk_pct, hp_pct)
|
||||
if not adr.start_adventure(seed_val):
|
||||
if not adr.start_adventure(seed_val, {"hero_override": hero_override}):
|
||||
st.result = "start_failed"
|
||||
if own_session:
|
||||
logger.close_session()
|
||||
@@ -142,6 +147,7 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
|
||||
st.visited_ids.append(int(r.node_id_of(node)))
|
||||
if t == "battle" or t == "boss":
|
||||
st.battles += 1
|
||||
st.last_battle_enemies = [] # 末场战斗敌方名序列按战斗重置(失败局终值 = 死亡遭遇)
|
||||
st.encounter_max_hp_seq.append(int(r.effective_max_hp()))
|
||||
if logger != null and r.current_battle() != null:
|
||||
logger.attach_battle(r.current_battle())
|
||||
@@ -197,16 +203,33 @@ static func _route_pick(runner, route: String) -> int:
|
||||
static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows: bool) -> void:
|
||||
if row.type == "damage" and row.source_class == "enemy_action":
|
||||
st.hero_dmg += int(row.value)
|
||||
_note_enemy(String(row.get("text", "")), st)
|
||||
elif row.type == "cast" and row.source_class == "enemy_action":
|
||||
_note_enemy(String(row.get("text", "")), st) # 施放行(毒爆挂毒等——伤害行可能缺席的死亡遭遇仍可归因)
|
||||
if row.source_class == "mine_hit":
|
||||
st.mine_hits += 1 # 踩雷证据行(负向兑付——完美档断言恒 0;盲挖惩罚链埋点)
|
||||
if collect_rows:
|
||||
st.rows.append(row)
|
||||
|
||||
|
||||
# 敌方显示名采集(数值验证工具批 2026-09-22):敌方行动/施放行文本首词 = 单位显示名
|
||||
#(攻击行「地精 撕咬(暴击)」、施放行「地精 施放 毒雾」——显示名不含空格,去重连续重复)
|
||||
static func _note_enemy(text: String, st: Dictionary) -> void:
|
||||
if text.is_empty():
|
||||
return
|
||||
var ename := text.split(" ")[0]
|
||||
if ename.is_empty():
|
||||
return
|
||||
var arr: Array = st.last_battle_enemies
|
||||
if arr.is_empty() or String(arr[arr.size() - 1]) != ename:
|
||||
arr.append(ename)
|
||||
|
||||
|
||||
static func _on_node_entered(node: Dictionary, info: Dictionary, runner, st: Dictionary, logger) -> void:
|
||||
var t := String(node.get("type", ""))
|
||||
if t == "battle" or t == "boss":
|
||||
st.battles += 1
|
||||
st.last_battle_enemies = [] # 末场战斗敌方名序列按战斗重置(失败局终值 = 死亡遭遇)
|
||||
if logger != null and runner.current_battle() != null:
|
||||
logger.attach_battle(runner.current_battle())
|
||||
if t == "boss":
|
||||
@@ -228,6 +251,7 @@ static func _on_level_finished(p: Dictionary, st: Dictionary, logger) -> void:
|
||||
st.result = String(p.get("result", ""))
|
||||
st.hp_left = int(p.get("hp", 0))
|
||||
st.defeat_node_index = int(p.get("node_index", -1))
|
||||
st.defeat_node_id = int(p.get("node_id", -1)) # 编号空间解析与行进同源(图版 = node_depth——直入口死亡节点可读面)
|
||||
st.defeat_node_type = String(p.get("node_type", ""))
|
||||
if logger != null:
|
||||
logger.log_level_end(p)
|
||||
|
||||
@@ -219,6 +219,10 @@ func _smoke_adventure() -> void:
|
||||
check(not bool(sl["cleared"]) and int(sl["levels_cleared"]) == 0 \
|
||||
and int(sl["keystone_total"]) == 0 and int(sl["stopped_at"]) == 1,
|
||||
"L1 败北摘要:未打穿 · 通关 0 关 · 钥石 0 · 止步第 1 层")
|
||||
var dl: Dictionary = sl.get("defeat", {})
|
||||
check(int(dl.get("level_index", -1)) == 1 and int(dl.get("node_id", -1)) >= 0 \
|
||||
and String(dl.get("node_type", "")) in ["battle", "boss"],
|
||||
"L1 败北败退明细:层号/节点编号/节点类型在案(数值验证取数面 2026-09-22)")
|
||||
var half := {}
|
||||
for s in range(42, 142):
|
||||
var sth: Dictionary = autoplay.run_adventure(_db, s, 50, 0)
|
||||
@@ -232,6 +236,10 @@ func _smoke_adventure() -> void:
|
||||
and int(sh["keystone_total"]) == int(_db.adventure.keystone_per_level) \
|
||||
and int(sh["stopped_at"]) == 2,
|
||||
"L2 败北摘要:L1 已过(钥石 1×表值)· 止步第 2 层")
|
||||
var dh: Dictionary = sh.get("defeat", {})
|
||||
check(int(dh.get("level_index", -1)) == 2 and int(dh.get("node_id", -1)) >= 0 \
|
||||
and String(dh.get("node_type", "")) in ["battle", "boss"],
|
||||
"L2 败北败退明细:层号=2 · 节点在案(数值验证取数面 2026-09-22)")
|
||||
# ⑥ 重复宣布拒绝(settle 后再调 _on_level_finished——摘要零变化)
|
||||
var adr6 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(7))
|
||||
adr6.set_hero_override(400, 200)
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-22(数值验证工具批——判定端 tools/balance_verifier.py 的数据源,本文件零判定逻辑)
|
||||
# 三段一次采集,stdout 以「VJSON 」前缀行出机器读 JSON(其余行为人读进度/摘要):
|
||||
# level 段 直入口单关基线(level_autoplay.run_level 零加成)——L1/L2/L3 × 双画像 × n,
|
||||
# 供验收带判定(分带批 2026-09-20 口径:L1 普通 [35,55] / L2 普通 [25,45] / 完美 [35,65])
|
||||
# adventure 段 全冒险连探基线(run_adventure 零永久强化)× 双画像 × na——止步分布与败退节点
|
||||
# (摘要 defeat 明细 + 末场战斗敌方名),回答「零加成倒在第几层哪个节点」
|
||||
# campaign 段 营地循环战役 × 双画像 × k 局——SaveManager 隔离档 + MetaProgression 真实账本:
|
||||
# 逐局 hero_override() 注入 → 冒险 → settle_adventure 入账 → 花费策略买锻造,
|
||||
# 回答「累计几次通关冒险后下一关可过」(元进度换算率标定的模拟读数源)
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/verifier_batch.gd -- [k=v ...]
|
||||
# n=60(直入口每格局数) na=40(基线冒险局数) k=40(战役局数) seed0=8000
|
||||
# only=level,adventure,campaign(缺省全段) spend=hp_first|atk_first(锻造花费优先序)
|
||||
# 种子纪律:unique 逐局独立(掷点测量修复批 A 案——seed0 + 全局游标,每掷点恰消费一次)。
|
||||
# 日志合规:全段共享 playtest_logger 会话(与真人试玩同日志),分析工具可直接消费。
|
||||
# 存档红线:战役档用 user://verifier_campaign_<画像>.json 隔离路径,段首清陈旧段尾清理——
|
||||
# 绝不碰 user://save.json(与 smoke 存档隔离纪律同源)。
|
||||
extends SceneTree
|
||||
|
||||
const SPEND_ORDERS := {
|
||||
"hp_first": ["max_hp", "atk", "armor_class"],
|
||||
"atk_first": ["atk", "max_hp", "armor_class"],
|
||||
}
|
||||
|
||||
var _cursor := 0 # unique 种子全局游标(三段连续消费,段间不重叠)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var opt := _parse_args(OS.get_cmdline_user_args())
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
var logger = load("res://presentation/playtest_logger.gd").new()
|
||||
logger.ap_per_round = int(db.action_economy.ap_per_round)
|
||||
var log_path: String = logger.start_session(opt.seed0)
|
||||
print("模拟日志: %s" % log_path)
|
||||
var autoplay = load("res://smoke/level_autoplay.gd")
|
||||
var t0 := Time.get_ticks_msec()
|
||||
var counts := {"level": 0, "adventure": 0, "campaign": 0}
|
||||
if "level" in opt.only:
|
||||
_seg_level(db, autoplay, logger, opt, counts)
|
||||
if "adventure" in opt.only:
|
||||
_seg_adventure(db, autoplay, logger, opt, counts)
|
||||
if "campaign" in opt.only:
|
||||
_seg_campaign(db, autoplay, logger, opt, counts)
|
||||
logger.close_session()
|
||||
print("数值验证采集完成:level %d 局 · adventure %d 局 · campaign %d 局,耗时 %.1f 秒"
|
||||
% [counts.level, counts.adventure, counts.campaign, float(Time.get_ticks_msec() - t0) / 1000.0])
|
||||
quit(0)
|
||||
|
||||
|
||||
func _parse_args(args: PackedStringArray) -> Dictionary:
|
||||
var opt := {"n": 60, "na": 40, "k": 40, "seed0": 8000, "only": "level,adventure,campaign", "spend": "hp_first"}
|
||||
for a in args:
|
||||
var kv := String(a).split("=", true, 1)
|
||||
if kv.size() != 2:
|
||||
continue
|
||||
var key := String(kv[0])
|
||||
if opt.has(key):
|
||||
if key == "only":
|
||||
opt[key] = String(kv[1])
|
||||
elif key == "spend":
|
||||
opt[key] = String(kv[1])
|
||||
else:
|
||||
opt[key] = int(kv[1])
|
||||
var segs: Array = []
|
||||
for s in String(opt.only).split(","):
|
||||
if s in ["level", "adventure", "campaign"]:
|
||||
segs.append(s)
|
||||
opt["only"] = segs
|
||||
if not SPEND_ORDERS.has(String(opt.spend)):
|
||||
opt["spend"] = "hp_first"
|
||||
return opt
|
||||
|
||||
|
||||
func _next_seed(seed0: int) -> int:
|
||||
var s: int = seed0 + _cursor
|
||||
_cursor += 1
|
||||
return s
|
||||
|
||||
|
||||
func _emit(row: Dictionary) -> void:
|
||||
print("VJSON " + JSON.stringify(row))
|
||||
|
||||
|
||||
# ---- level 段:直入口单关基线(零加成——分带批验收带判定的同口径读数) ----
|
||||
|
||||
func _seg_level(db, autoplay, logger, opt: Dictionary, counts: Dictionary) -> void:
|
||||
for policy in ["normal", "perfect"]:
|
||||
for li in [1, 2, 3]:
|
||||
for i in range(int(opt.n)):
|
||||
var seed_val := _next_seed(int(opt.seed0))
|
||||
var st: Dictionary = autoplay.run_level(db, seed_val, li, 0, 0, false, logger, policy, "min")
|
||||
_emit({
|
||||
"seg": "level", "level": li, "policy": policy, "seed": seed_val,
|
||||
"result": String(st.result),
|
||||
"defeat_node_id": int(st.defeat_node_id), "defeat_node_type": String(st.defeat_node_type),
|
||||
"battles": int(st.battles), "rounds": int(st.rounds),
|
||||
})
|
||||
counts.level += 1
|
||||
print("进度 level 第%d层 %s %d/%d" % [li, policy, int(opt.n), int(opt.n)])
|
||||
|
||||
|
||||
# ---- adventure 段:全冒险连探基线(零永久强化——止步分布与败退节点归因) ----
|
||||
|
||||
func _seg_adventure(db, autoplay, logger, opt: Dictionary, counts: Dictionary) -> void:
|
||||
for policy in ["normal", "perfect"]:
|
||||
for i in range(int(opt.na)):
|
||||
var seed_val := _next_seed(int(opt.seed0))
|
||||
var st: Dictionary = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, "min")
|
||||
_emit(_adventure_row(st, policy, seed_val))
|
||||
counts.adventure += 1
|
||||
print("进度 adventure %s %d/%d" % [policy, int(opt.na), int(opt.na)])
|
||||
|
||||
|
||||
# 冒险局共用取数行(adventure/campaign 两段同构——summary 缺席 = 编排异常局,原样带 result 供判定端计异常)
|
||||
func _adventure_row(st: Dictionary, policy: String, seed_val: int) -> Dictionary:
|
||||
var sum: Dictionary = st.summary
|
||||
var row := {
|
||||
"seg": "adventure", "policy": policy, "seed": seed_val,
|
||||
"result": String(st.result),
|
||||
"levels_cleared": int(sum.get("levels_cleared", -1)),
|
||||
"stopped_at": int(sum.get("stopped_at", -1)),
|
||||
"keystone": int(sum.get("keystone_total", -1)),
|
||||
"defeat": sum.get("defeat", {}),
|
||||
"last_enemies": st.last_battle_enemies,
|
||||
}
|
||||
if not row.defeat.is_empty() and not st.last_battle_enemies.is_empty():
|
||||
row.defeat["enemies"] = st.last_battle_enemies
|
||||
return row
|
||||
|
||||
|
||||
# ---- campaign 段:营地循环战役(隔离档真实账本——逐局注入/收束/购买全走正式侧路径) ----
|
||||
|
||||
func _seg_campaign(db, autoplay, logger, opt: Dictionary, counts: Dictionary) -> void:
|
||||
for policy in ["normal", "perfect"]:
|
||||
var save_path := "user://verifier_campaign_%s.json" % policy
|
||||
_remove_save(save_path) # 段首清陈旧(新档 = 白板存档)
|
||||
var save = load("res://core/save_manager.gd").new(save_path)
|
||||
var meta = load("res://core/meta_progression.gd").new(db, save)
|
||||
for r in range(int(opt.k)):
|
||||
var seed_val := _next_seed(int(opt.seed0))
|
||||
var ov: Dictionary = meta.hero_override()
|
||||
var keystone_before: int = meta.keystone_balance()
|
||||
var st: Dictionary = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, "min", ov)
|
||||
var row := _adventure_row(st, policy, seed_val)
|
||||
row["seg"] = "campaign"
|
||||
row["run_no"] = r + 1
|
||||
row["override"] = ov
|
||||
row["keystone_before"] = keystone_before
|
||||
if st.summary.is_empty():
|
||||
# 冒险未正常结算(timeout/advance_failed——编排异常零入账零购买,判定端计异常)
|
||||
row["anomaly"] = true
|
||||
_emit(row)
|
||||
counts.campaign += 1
|
||||
continue
|
||||
var settle: Dictionary = meta.settle_adventure(st.summary)
|
||||
row["keystone_income"] = int(settle.total)
|
||||
row["purchases"] = _spend(meta, String(opt.spend))
|
||||
row["keystone_after"] = meta.keystone_balance()
|
||||
row["forge"] = {"max_hp": meta.forge_level("max_hp"), "atk": meta.forge_level("atk"),
|
||||
"armor_class": meta.forge_level("armor_class")}
|
||||
row["new_unlocks"] = settle.new_unlocks
|
||||
_emit(row)
|
||||
counts.campaign += 1
|
||||
if (r + 1) % 10 == 0:
|
||||
print("进度 campaign %s %d/%d(锻造 血%d/攻%d/甲%d · 钥石 %d)" % [policy, r + 1, int(opt.k),
|
||||
meta.forge_level("max_hp"), meta.forge_level("atk"), meta.forge_level("armor_class"),
|
||||
meta.keystone_balance()])
|
||||
_remove_save(save_path) # 段尾清理(与 smoke 存档纪律同源——不遗留隔离档)
|
||||
|
||||
|
||||
# 花费策略(贪心——按优先序反复买当前可负担的最高优先项;几何递增定价下单档连买价格上行自然外溢到次档)
|
||||
func _spend(meta, policy_name: String) -> Array:
|
||||
var out: Array = []
|
||||
var order: Array = SPEND_ORDERS.get(policy_name, SPEND_ORDERS["hp_first"])
|
||||
var guard := 200
|
||||
while guard > 0:
|
||||
guard -= 1
|
||||
var bought := false
|
||||
for attr in order:
|
||||
var price: int = meta.forge_price(attr)
|
||||
if price > 0 and meta.keystone_balance() >= price and meta.buy_forge(attr):
|
||||
out.append({"attr": attr, "tier": int(meta.forge_level(attr)), "price": price})
|
||||
bought = true
|
||||
break
|
||||
if not bought:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
func _remove_save(path: String) -> void:
|
||||
if FileAccess.file_exists(path):
|
||||
DirAccess.remove_absolute(path)
|
||||
@@ -0,0 +1 @@
|
||||
uid://datax1elpwkik
|
||||
Reference in new issue
Block a user