Files
rougeGame/Tools/Unreal/PreparePresentation.py

240 lines
13 KiB
Python
Raw Permalink 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.
"""Reproducible presentation derivatives; editable originals stay in SourceAssets."""
import hashlib
import json
import math
import random
import re
import shutil
import struct
import wave
import zipfile
from pathlib import Path
import resvg_py
from fontTools.ttLib import TTFont
from fontTools.varLib.instancer import instantiateVariableFont
ROOT=Path(__file__).resolve().parents[2]
STRINGS="""
subtitle=机甲搜打撤 · 轨道边境|MECH EXTRACTION / ORBITAL FRONTIER
baseTitle=机库指挥台|HANGAR COMMAND
enterHangar=进入机库|ENTER HANGAR
deploy=整备并出击|DEPLOY
resume=继续出击|RESUME SORTIE
training=试验场|TRAINING RANGE
research=研究与制造|RESEARCH & FABRICATION
settings=设置|SETTINGS
performance=性能统计|PERFORMANCE
perfOn=开启|ON
perfOff=关闭|OFF
perfPosition=显示位置|POSITION
perfTopRight=右上角|TOP RIGHT
perfTopLeft=左上角|TOP LEFT
perfBottomRight=右下角|BOTTOM RIGHT
perfBottomLeft=左下角|BOTTOM LEFT
perfHint=60 FPS 参考预算 16.67 ms · — 表示暂无数据|60 FPS BUDGET: 16.67 ms / — MEANS UNAVAILABLE
perfFps=FPS|FPS
perfFrame=帧耗时|FRAME
perfGpu=GPU|GPU
perfGame=CPU 游戏|CPU GAME
perfRender=CPU 渲染|CPU RENDER
perfRhi=RHI|RHI
quit=退出游戏|EXIT GAME
baseHint=整备装备,搜索物资,活着撤离。|Equip. Search. Extract alive.
pilot=驾驶员档案|PILOT DOSSIER
warehouse=基地仓库|BASE WAREHOUSE
pending=待整理物资|RECOVERY INBOX
sorties=出击|SORTIES
returns=撤离|RETURNS
activeRun=任务进行中 · 可继续|ACTIVE SORTIE / RESUMABLE
standby=基地待命|BASE STANDBY
carryHint=拖动放置 · R 旋转 · 安全箱机毁保全|DRAG TO PLACE / R ROTATES / SECURE CASE SURVIVES
fieldSystems=战地改装与回收|FIELD SYSTEMS
fieldHint=安装实物装备,选择货舱物品后回收|INSTALL REAL PARTS / SELECT CARGO FOR RECOVERY
fieldEquipment=当前实物装备 · 可战地改装|INSTALLED PARTS / FIELD MODIFICATIONS
warehouseHint=滚轮浏览 · 有限容量 · 物品唯一归属|SCROLL TO BROWSE / LIMITED CAPACITY / UNIQUE ITEMS
primary=主武器|PRIMARY
secondary=副武器|SECONDARY
booster=推进器|BOOSTER
guard=防御模块|GUARD
mass=载重|MASS
power=功率|POWER
cargo=货仓|CARGO
safeCase=安全箱 · 2 × 2|SECURE CASE / 2 × 2
cellsUsed=格已用|CELLS USED
sort=整理仓库|SORT STASH
podSelect=选择主动回收物品|SELECT RECOVERY CARGO
podRule=每次出击一舱 · 2 × 2 · 最多四件|ONE POD PER SORTIE / 2 × 2 / UP TO FOUR ITEMS
holdPod=关闭物品栏,落地静止长按|CLOSE INVENTORY, STAND STILL, HOLD
selected=已选择|SELECTED
uninstall=卸入货仓|UNINSTALL TO CARGO
borrow=借用基础件|BORROW BASELINE PART
borrowed=借用件|BORROWED
equip=装备此物品|EQUIP ITEM
toCargo=预装到货仓|PREPARE CARGO
toSafe=预装到安全箱|PREPARE SECURE CASE
unprepare=取消预装|CANCEL PREPARATION
install=安装装备|INSTALL PART
transfer=转移至另一容器|TRANSFER CONTAINER
recycle=回收已研究蓝图|RECYCLE KNOWN BLUEPRINT
dragHint=拖拽摆放 · R 旋转 · 数量可拆分|DRAG / R ROTATES / QUANTITY SPLITS STACKS
selectItem=选择物品查看操作|SELECT AN ITEM FOR ACTIONS
back=返回|BACK
researchHint=研究消耗合金 2、电芯 1;制造按配方消耗实物材料。|RESEARCH COSTS 2 ALLOY + 1 CELL. FABRICATION USES PHYSICAL STOCK.
manufacture=制造装备|FABRICATE PART
inboxHint=仓库满时物资保留在此;整理完才能再次出击。|OVERFLOW IS PRESERVED HERE. CLEAR INBOX BEFORE A NEW SORTIE.
claim=收入仓库|CLAIM
claimAll=全部收入仓库|CLAIM ALL
discard=丢弃|DISCARD
confirmDiscard=再次点击丢弃以确认。|CLICK DISCARD AGAIN TO CONFIRM.
settingsHint=显示、鼠标、声音与键位立即生效并保存。|DISPLAY, MOUSE, AUDIO AND KEY BINDINGS APPLY AND SAVE.
display=显示|DISPLAY
fullscreen=切换全屏 / 窗口|TOGGLE FULLSCREEN / WINDOWED
sensitivity=鼠标灵敏度|MOUSE SENSITIVITY
aimSensitivity=瞄准灵敏度倍率|AIM SENSITIVITY
fire=主武器开火|PRIMARY FIRE
aim=瞄准|AIM
invertY=反转鼠标纵轴|INVERT MOUSE Y
controls=键鼠操作|KEYBOARD & MOUSE
forward=向前|FORWARD
backward=向后|BACKWARD
left=向左|LEFT
right=向右|RIGHT
jump=跳跃 / 轻型悬浮|JUMP / LIGHT-CHASSIS LIFT
boost=推进冲刺|BOOST
reload=装填|RELOAD
view=切换第一 / 越肩视角|COCKPIT / SHOULDER VIEW
interact=交互 / 登机|INTERACT / BOARD
inventory=携带物品|INVENTORY
pod=主动回收(长按)|RECOVERY POD (HOLD)
pressKey=请按新按键 · Esc 取消|PRESS A KEY / ESC CANCELS
keyConflict=该按键已被其他操作使用。|THAT KEY IS ALREADY ASSIGNED.
volume=音量|VOLUME
quality=切换画质等级|CYCLE QUALITY
paused=任务暂停|SORTIE PAUSED
upgrade=局内改装|FIELD UPGRADE
pauseHint=查看携带物资,或保存进度后返回基地。|REVIEW YOUR CARGO OR SAVE YOUR SORTIE AND RETURN TO BASE.
saveAndHome=保存并返回主页|SAVE & RETURN HOME
upgradeDamage=火力强化 · 提升伤害|FIREPOWER / INCREASE DAMAGE
upgradeRepair=战地修复 · 恢复装甲|FIELD REPAIR / RESTORE ARMOR
upgradeEnergy=能源维护 · 恢复能源|ENERGY SERVICE / RESTORE ENERGY
armor=装甲|ARMOR
energy=能源|ENERGY
stage=区域|SECTOR
salvage=携带|CARGO
routeDone=路线完成,仍需主动撤离|ROUTE COMPLETE / EXTRACT TO BANK LOOT
objectiveDone=目标完成,可前往下一片区|OBJECTIVE COMPLETE / NEXT SECTOR UNLOCKED
findObjective=搜索物资并寻找目标终端|SEARCH SALVAGE AND FIND THE OBJECTIVE TERMINAL
podUsed=主动回收舱:已使用|RECOVERY POD: USED
podAvailable=主动回收舱:1 / 1|RECOVERY POD: 1 / 1
reloading=装填中|RELOADING
wait=接应抵达倒计时|EXTRACTION ARRIVES IN
podHolding=货舱回收准备中,保持静止|RECOVERY ARMING / HOLD POSITION
footer=键鼠操作 · Esc 返回 · 搜索,战斗,撤离|KEYBOARD & MOUSE / ESC BACK / SEARCH, FIGHT, EXTRACT
completed=完成路线|COMPLETED ROUTES
rangeStats=试验场 · 命中 / 关卡 / 用时|RANGE / HITS / GATES / TIME
rangeReset=试验场计时和命中已重置。|RANGE TIMER AND HITS RESET.
courseGate=机动计时门|TIMED GATE
extraction=撤离平台|EXTRACTION PAD
routeBoss=首领路线|BOSS ROUTE
routeEscort=护送路线|ESCORT ROUTE
rangeResetLabel=重置试验场|RESET RANGE
saved=操作已保存|SAVED
invalid=该操作当前不可用。|THAT ACTION IS NOT AVAILABLE.
missing=找不到对应物品。|ITEM NOT FOUND.
noSpace=空间不足或物品无法放入。|NOT ENOUGH SPACE OR ITEM DOES NOT FIT.
locked=当前状态禁止该操作。|ACTION LOCKED IN THE CURRENT STATE.
budget=装备超出底盘载重或功率预算。|LOADOUT EXCEEDS CHASSIS MASS OR POWER BUDGET.
funds=可用材料不足。|INSUFFICIENT AVAILABLE MATERIALS.
duplicate=该物资或事件已经处理。|THIS SOURCE OR EVENT HAS ALREADY BEEN PROCESSED.
saveFailed=保存失败,操作未结算。请检查磁盘。|SAVE FAILED. TRANSACTION NOT COMMITTED. CHECK DISK.
corrupt=存档及备份无法读取,已保留原文件。|PROFILE AND BACKUP UNREADABLE. ORIGINAL FILES PRESERVED.
unsupported=存档版本不受支持。|UNSUPPORTED SAVE VERSION.
destroyed=机体损毁。安全箱已保全,其余物资损失。|MECH DESTROYED. SECURE CASE PRESERVED; OTHER CARGO LOST.
extracted=撤离成功,物资已回收入库。|EXTRACTED. RECOVERED ITEMS BANKED.
nothingNear=靠近物资箱、终端或撤离点后交互。|MOVE CLOSER TO SALVAGE, TERMINALS OR EXTRACTION.
loot=已搜索物资|SALVAGE RECOVERED
called=已呼叫接应,等待后需主动登机。|EXTRACTION CALLED. WAIT, THEN BOARD MANUALLY.
finalObjective=完成首领战或护送后仍需主动撤离。|DEFEAT THE BOSS OR COMPLETE ESCORT, THEN EXTRACT.
escort=护送运输车|ESCORT TRANSPORT
explore=搜索箱体 · 击败敌人 · 终端推进 · 绿色平台撤离|SEARCH CRATES / FIGHT / ACTIVATE TERMINALS / GREEN PAD EXTRACTS
search=搜索|SEARCH
board=登机并撤离|BOARD & EXTRACT
call=呼叫接应|CALL EXTRACTION
nextStage=前往下一片区(右侧为护送路线)|NEXT SECTOR (RIGHT GATE: ESCORT ROUTE)
activate=启动目标终端|ACTIVATE OBJECTIVE TERMINAL
readyToBoard=接应已抵达,回到平台按交互键登机。|TRANSPORT READY. RETURN TO PAD AND PRESS INTERACT TO BOARD.
podSent=选定货舱物资已发射回收,本次回收舱已用尽。|SELECTED CARGO RECOVERED. THIS SORTIE'S POD IS USED.
podInterrupted=回收被移动、起跳或命中打断。|RECOVERY INTERRUPTED BY MOVEMENT, AIRBORNE STATE OR DAMAGE.
escortLost=护送运输车已损毁。|ESCORT TRANSPORT DESTROYED.
"""
def prepare_localization():
raw=ROOT/"Content/IRONSPORE/Raw"
pairs={}
for line in STRINGS.strip().splitlines():
key,text=line.split("=",1)
pairs["ue."+key]=text.split("|",1)
required=set()
for file in (ROOT/"Source").rglob("*.cpp"):
required.update(re.findall(r'TEXT\("(ue\.[^" ]+)"\)',file.read_text(encoding="utf-8")))
if required-pairs.keys():
raise RuntimeError("Missing strings: "+str(sorted(required-pairs.keys())))
for index,locale in enumerate(("zh-Hans","en")):
data=json.loads((ROOT/f"SourceAssets/Localization/Tables/{locale}.json").read_text(encoding="utf-8"))
entries={item["key"]:item["value"] for item in data["entries"]}
entries.update({key:values[index] for key,values in pairs.items()})
folder=raw/"Localization";folder.mkdir(exist_ok=True)
(folder/f"{locale}.json").write_text(json.dumps({"entries":[{"key":k,"value":v} for k,v in entries.items()]},ensure_ascii=False,indent=2)+"\n",encoding="utf-8")
def prepare():
raw=ROOT/"Content/IRONSPORE/Raw"
(raw/"Fonts").mkdir(parents=True,exist_ok=True)
font=TTFont(ROOT/"SourceAssets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf")
font=instantiateVariableFont(font,{"wght":400},inplace=True)
for name in font["name"].names:
if name.nameID in (1,4,6,16,17):
value="Regular" if name.nameID==17 else "IronsSansSC-Regular" if name.nameID==6 else "Irons Sans SC"
name.string=value.encode(name.getEncoding())
font.recalcTimestamp=False
font.save(raw/"Fonts/IronsSansSC-Regular.ttf")
prepare_localization()
derived=ROOT/"SourceAssets/Derived/UI";derived.mkdir(parents=True,exist_ok=True)
for svg in (ROOT/"SourceAssets/UI").rglob("*.svg"):
(derived/f"T_{svg.stem}.png").write_bytes(resvg_py.svg_to_bytes(svg_path=str(svg),zoom=2,skip_system_fonts=True))
audio=ROOT/"SourceAssets/Audio/Generated";audio.mkdir(parents=True,exist_ok=True)
rng=random.Random(7001)
for name,frequency,duration in [("S_Shot",140,.12),("S_Impact",75,.18),("S_Secondary",65,.42),("S_UI",700,.08)]:
rate=44100;samples=[]
for n in range(int(rate*duration)):
t=n/rate;decay=(1-t/duration)**3
value=(math.sin(2*math.pi*frequency*t*(1-.5*t/duration))*.55+rng.uniform(-1,1)*.45)*decay*.65
samples.append(struct.pack("<h",round(value*32767)))
with wave.open(str(audio/(name+".wav")),"wb") as wav:
wav.setparams((1,2,rate,0,"NONE","not compressed"));wav.writeframes(b"".join(samples))
archive=next((ROOT/"SourceAssets/ThirdParty/resvg-py/0.5.0").glob("*.whl"))
with zipfile.ZipFile(archive) as wheel:
license_data=wheel.read(next(n for n in wheel.namelist() if n.endswith("/LICENSE")))
(ROOT/"Docs/Production/Licenses/resvg-py-0.5.0-MIT.txt").write_bytes(license_data)
record={"tool":"resvg-py","version":"0.5.0","license":"MIT","author":"baseplate-admin","source":"https://pypi.org/project/resvg-py/0.5.0/","archive":str(archive.relative_to(ROOT)).replace("\\","/"),"sha256":hashlib.sha256(archive.read_bytes()).hexdigest(),"use":"Build-time conversion of project-owned editable SVGs; not shipped as runtime code","outputs":[]}
for path in sorted(derived.glob("*.png"))+sorted(audio.glob("*.wav")):
record["outputs"].append({"path":str(path.relative_to(ROOT)).replace("\\","/"),"sha256":hashlib.sha256(path.read_bytes()).hexdigest(),"author":"IRONSPORE project","license":"project-owned"})
font_archive=next((ROOT/"SourceAssets/ThirdParty/fonttools/4.55.3").glob("*.whl"))
with zipfile.ZipFile(font_archive) as package:
license_data=package.read(next(n for n in package.namelist() if n.endswith("/LICENSE")))
(ROOT/"Docs/Production/Licenses/fonttools-4.55.3-LICENSE.txt").write_bytes(license_data)
record["fontDerivative"]={"tool":"fonttools","version":"4.55.3","author":"Just van Rossum and contributors","license":"MIT","source":"https://pypi.org/project/fonttools/4.55.3/","archive":str(font_archive.relative_to(ROOT)).replace("\\","/"),"sha256":hashlib.sha256(font_archive.read_bytes()).hexdigest(),"operation":"Instance weight 400, renamed family Irons Sans SC; original copyright and SIL OFL retained","output":"Content/IRONSPORE/Raw/Fonts/IronsSansSC-Regular.ttf","outputSha256":hashlib.sha256((raw/"Fonts/IronsSansSC-Regular.ttf").read_bytes()).hexdigest()}
(ROOT/"Docs/Production/PresentationSources.json").write_text(json.dumps(record,ensure_ascii=False,indent=2)+"\n",encoding="utf-8")
for source,name in ((ROOT/"SourceAssets/ThirdParty/nlohmann/json/3.12.0/LICENSE.MIT","nlohmann-json-3.12.0-MIT.txt"),(ROOT/"SourceAssets/ThirdParty/Adobe/NotoSansSC/OFL.txt","NotoSansSC-OFL.txt")):
shutil.copyfile(source,ROOT/"Docs/Production/Licenses"/name)
shutil.copyfile(source,raw/name)
print("Prepared bilingual keys, 10 PNG derivatives, 4 generated WAVs")
if __name__=="__main__":
import argparse
parser=argparse.ArgumentParser();parser.add_argument("--localization-only",action="store_true")
if parser.parse_args().localization_only:prepare_localization()
else:prepare()