Files
2026-10-16 23:33:13 -07:00

168 lines
6.8 KiB
Python

"""Regenerate and audit levelup-rewards.json from the 2023 catalogue.
The server enforces two things over a level-up box: the deny-list in
Server/Utils/LevelUpRewardPool.cs, which can never be overridden, and the allow-list in
levelup-rewards.json, which is the reviewed set of items a box may contain. This script rebuilds
that allow-list and prints what it kept and what it refused, so a change to the catalogue or the
deny-list can be reviewed as a diff.
The patterns below mirror LevelUpRewardPool.DeniedPatterns and DeniedTags and MUST be kept in step
with them. The server's copy is the source of truth - the allow-list is already filtered by it at
pick time, so a pattern missing here only means a candidate that the server would still refuse;
it can never mean an item the server would allow. Run this from anywhere with the repo checked out:
python Server/RadicadeServer-Config/generate-levelup-rewards.py
Then read the audit, delete anything from the JSON that should not be there, and remember that an
item with no name, tooltip or tag to judge it by cannot be classified here at all.
"""
import json
import os
import re
# The script sits beside the files it reads, so it works from any working directory.
CONFIG = os.path.dirname(os.path.abspath(__file__))
DENIED = [
"exclusive", "early access", "alpha", "art festival",
"rec con", "reccon", "rrcc", "raw data", "sfvrcc",
"contest", "trophy", "competition",
"moderator", "volunteer",
"video partner", "creator program", "partnered creator", "partner",
"bug bounty", "security", "ethical hackers", "gold notes",
"seller swag", "sponsored league",
"rro-", "rro specific",
"promo", "promotional", "sponsor", "collab", "licensed", "prom",
"mrbeast", "mr beast", "miraculous", "nba",
"class of", "graduation", "class)", "creator conference",
"for earning", "available until", "unlocked by visiting",
"unlocked during", "xbox", "launch reward", "launch exclusive",
"reward for", "given to", "space marine", "space visor",
"(gold)",
"limited", "never return", "founder", "founders",
"halloween", "holiday", "thanksgiving", "invasion", "elseware",
"new year", "carnivale", "reclympics", "rec rocks", "anniversary", "2020 glasses",
"developer", "dev only", "staff only", "admin only", "test item", "debug",
]
DENIED_TAGS = {"halloween", "holidays", "thanksgiving", "invasion", "elseware", "reccon", "reccon2021"}
def denied_name(text):
if not text:
return None
low = text.lower()
for p in DENIED:
if p in low:
return p
return None
def denied(item):
for field in ("FriendlyName", "Tooltip"):
hit = denied_name(item.get(field) or "")
if hit:
return field + ":" + hit
tags = (item.get("TagList") or "").lower()
for tag in re.split(r"[,\s]+", tags):
if tag.strip() in DENIED_TAGS:
return "tag:" + tag.strip()
return None
def main():
catalog = json.load(open(CONFIG + r"\2022-avatar.json", encoding="utf-8"))
resolvable = set(
d.strip().lower()
for d in json.load(open(CONFIG + r"\avatar-item-lookup.json", encoding="utf-8"))["descriptions"]
)
kept, refused = [], {}
for item in catalog:
desc = (item.get("AvatarItemDesc") or "").strip()
if not desc or item.get("IsBaseAvatarItem"):
continue
rarity = item.get("Rarity", 0)
if rarity not in (10, 20, 30, 50):
refused.setdefault("rarity", []).append(item.get("FriendlyName"))
continue
if desc.lower() not in resolvable:
refused.setdefault("unresolvable", []).append(item.get("FriendlyName"))
continue
why = denied(item)
if why:
refused.setdefault(why.split(":")[0], []).append(item.get("FriendlyName"))
continue
kept.append((rarity, item.get("FriendlyName") or "", desc))
kept.sort(key=lambda row: (row[0], row[1].lower()))
per_tier = {}
for rarity, _, _ in kept:
per_tier[rarity] = per_tier.get(rarity, 0) + 1
# ---- consumables: the shipped global list minus the deny-list, the premium and the seasonal
consumables = json.load(open(CONFIG + r"\consumables.json", encoding="utf-8"))
# Event, survey, licensed and paid-category consumables that no name pattern can identify.
extra_denied = [
"hair dye", "coupon", "snowman", "sparkler",
"dracula", "goblin king", "ghost beard", "witch film",
"surve", "survey", "forbidden", "forsaken", "premium", "creator bubbly",
]
consumable_kept, consumable_refused = [], {}
for item in consumables:
name = item.get("FriendlyName") or ""
why = denied_name(name)
if not why:
for pattern in extra_denied:
if pattern in name.lower():
why = pattern
break
if why:
consumable_refused.setdefault(why, []).append(name)
else:
consumable_kept.append((name, item["ConsumableItemDesc"]))
out = {
"AvatarItems": [desc for _, _, desc in kept],
"Consumables": [desc for _, desc in consumable_kept],
}
path = CONFIG + r"\levelup-rewards.json"
with open(path, "w", encoding="utf-8", newline="\r\n") as fh:
json.dump(out, fh, indent=2)
fh.write("\n")
print("wrote", path)
print("avatar allowed:", len(kept), "per rarity:", per_tier)
print("consumables allowed:", len(consumable_kept))
print()
print("== refused avatar items by reason ==")
for reason, names in sorted(refused.items(), key=lambda kv: -len(kv[1])):
print(f"{reason:20} {len(names):5} e.g. {names[:4]}")
print()
print("== refused consumables ==")
for reason, names in sorted(consumable_refused.items(), key=lambda kv: -len(kv[1])):
print(f"{reason:20} {len(names):5} {names[:6]}")
print()
print("== kept consumables ==")
for name, _ in consumable_kept:
print(" ", name)
print()
print("== sample of kept avatar items (first 2 per tier) ==")
seen = {}
for rarity, name, _ in kept:
if seen.get(rarity, 0) >= 2:
continue
seen[rarity] = seen.get(rarity, 0) + 1
print(f" {rarity}: {name}")
print(" ... a further", len(kept) - sum(seen.values()), "omitted")
print()
print("== denied check: items the brief names explicitly ==")
for probe in ["White Hat", "Winged Headphones", "Rec Con Hoodie", "Space Visor", "Royal Cape",
"Top Hat (Gold)", "Hallow's Blazer", "Prom", "MrBeast", "Early Access",
"Alpha Team", "Notes Headphones"]:
hits = [(i.get("FriendlyName"), denied(i)) for i in catalog
if probe.lower() in (i.get("FriendlyName") or "").lower()]
print(f"{probe!r}: " + (", ".join(f"{n}->{w}" for n, w in hits) if hits else "no match"))
main()