240 lines
13 KiB
Python
240 lines
13 KiB
Python
"""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()
|