nnnotes 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nnnotes/__init__.py +7 -0
- nnnotes/__main__.py +4 -0
- nnnotes/acb.py +190 -0
- nnnotes/addressables.py +413 -0
- nnnotes/adv.py +243 -0
- nnnotes/advcommand.py +29 -0
- nnnotes/advmedia.py +178 -0
- nnnotes/advscene.py +113 -0
- nnnotes/advui.py +1154 -0
- nnnotes/advvideo.py +684 -0
- nnnotes/atoms/__init__.py +151 -0
- nnnotes/atoms/astc.py +52 -0
- nnnotes/atoms/gltf.py +290 -0
- nnnotes/atoms/mesh.py +289 -0
- nnnotes/atoms/png.py +28 -0
- nnnotes/atoms/reader.py +269 -0
- nnnotes/atoms/sniff.py +198 -0
- nnnotes/atoms/sprite.py +236 -0
- nnnotes/atoms/texture.py +80 -0
- nnnotes/cache.py +289 -0
- nnnotes/catalog.py +373 -0
- nnnotes/catalogdb.py +457 -0
- nnnotes/census.py +276 -0
- nnnotes/cli.py +744 -0
- nnnotes/cli_assets.py +1539 -0
- nnnotes/config.py +238 -0
- nnnotes/contract.py +497 -0
- nnnotes/cri.py +371 -0
- nnnotes/crikey.py +71 -0
- nnnotes/crilips.py +265 -0
- nnnotes/cristages.py +558 -0
- nnnotes/deckdata.py +462 -0
- nnnotes/export.py +1462 -0
- nnnotes/gameapi.py +318 -0
- nnnotes/jsonio.py +98 -0
- nnnotes/languages.py +44 -0
- nnnotes/layout.py +596 -0
- nnnotes/link.py +418 -0
- nnnotes/live.py +115 -0
- nnnotes/live2d.py +235 -0
- nnnotes/liveaudio.py +331 -0
- nnnotes/livenotes.py +291 -0
- nnnotes/liveoptions.py +250 -0
- nnnotes/livescene.py +450 -0
- nnnotes/liveui.py +328 -0
- nnnotes/master.py +251 -0
- nnnotes/modelstages.py +424 -0
- nnnotes/motion.py +131 -0
- nnnotes/objexport.py +1564 -0
- nnnotes/orchestrate.py +630 -0
- nnnotes/plan.py +218 -0
- nnnotes/player.py +392 -0
- nnnotes/room.py +391 -0
- nnnotes/score.py +1139 -0
- nnnotes/shader.py +217 -0
- nnnotes/spot.py +201 -0
- nnnotes/stages.py +285 -0
- nnnotes/store.py +518 -0
- nnnotes/story.py +142 -0
- nnnotes/storyfonts.py +1258 -0
- nnnotes/storyhost.py +1190 -0
- nnnotes/storysite.py +662 -0
- nnnotes/storyvoices.py +102 -0
- nnnotes/textstyle.py +355 -0
- nnnotes/tmpfont.py +636 -0
- nnnotes/typetrees/6000.3.12f1.json +19 -0
- nnnotes/unity.py +368 -0
- nnnotes/viewrules.json +441 -0
- nnnotes/views.py +966 -0
- nnnotes/voices.py +1363 -0
- nnnotes/web.py +1624 -0
- nnnotes/webaudio.py +14 -0
- nnnotes/webmodel.py +397 -0
- nnnotes-0.1.0.dist-info/METADATA +200 -0
- nnnotes-0.1.0.dist-info/RECORD +79 -0
- nnnotes-0.1.0.dist-info/WHEEL +5 -0
- nnnotes-0.1.0.dist-info/entry_points.txt +2 -0
- nnnotes-0.1.0.dist-info/licenses/LICENSE +21 -0
- nnnotes-0.1.0.dist-info/top_level.txt +1 -0
nnnotes/livescene.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Live (rhythm game) stage/render data -> livescene/scene.json + textures + shaders.
|
|
2
|
+
|
|
3
|
+
What exists on the live screen before the first note:
|
|
4
|
+
|
|
5
|
+
player colour space, quality levels, URP pipelines, renderer data (player.py)
|
|
6
|
+
scene the addressable scene EmbScene/Live as a node list (Live prefab instance with
|
|
7
|
+
cameras, render canvas, background view and UI canvases; LiveGameView with
|
|
8
|
+
the lane camera, lane, note/effect containers; LiveStartTimelineDirector)
|
|
9
|
+
laneSkin lane skin sprites (lane_base, lane_tap_area, out_side_line) of the LiveSkinId option's
|
|
10
|
+
MasterLiveLaneSkin row
|
|
11
|
+
assets further prefabs by key: the start timeline of the band, the lane line prefab
|
|
12
|
+
master master rows the render path reads (music, quality settings, option defaults)
|
|
13
|
+
derived lane geometry computed with the game's formulas for the default options
|
|
14
|
+
(each entry names the function it mirrors)
|
|
15
|
+
postTextures film grain textures of the live renderers' post-process data
|
|
16
|
+
shaders every Shader in the loaded bundle closures, everything referenced, and the
|
|
17
|
+
shaders of the live camera renderers (GLES3 + other platforms, shader.py layout)
|
|
18
|
+
|
|
19
|
+
Only what the LightWeight screen (mode 3) loads is exported (LiveResourceLoadPipelineFactory.Create): the mode-1
|
|
20
|
+
stage (Band/{b}/live_stage/live_stage, its Volume Profile, StageBlinkLight), the background director and camera
|
|
21
|
+
blends are not loaded in mode 3; EmbLive/Volume/LiveStageVolume is referenced by nothing; the LiveGameVolume
|
|
22
|
+
profile is part of the scene (LiveEffectCamera's Volume).
|
|
23
|
+
|
|
24
|
+
Objects are exported with export.Exporter in its "livescene" clip format (clips inlined once,
|
|
25
|
+
bindings with crc32 keys, path candidates and curve counts).
|
|
26
|
+
|
|
27
|
+
Only the music row, the slice and the jacket sprite depend on the music: `band_part` exports the rest once for a
|
|
28
|
+
band and `music_part` completes it for each music of that band (the same document as `extract`).
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import math
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
from .export import Exporter
|
|
39
|
+
from .jsonio import write_json
|
|
40
|
+
from .liveoptions import OPTION_ITEM_TYPES
|
|
41
|
+
from .score import master_table
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --- keys -------------------------------------------------------------------
|
|
45
|
+
SCENE_KEY = "EmbScene/Live"
|
|
46
|
+
LANE_SKIN_PARTS = ("lane_base", "lane_tap_area", "out_side_line")
|
|
47
|
+
# asset keys exported whole ({band} = the band of the LightWeight background and the start timeline). The timeline
|
|
48
|
+
# prefab's PlayableDirector carries its TimelineAsset (Band/{b}/timeline/live_start_timeline_asset) inline.
|
|
49
|
+
ASSET_KEYS = {
|
|
50
|
+
"startTimeline": "Band/{band}/timeline/live_start_playable_timeline", # LiveStartTimelineLoadStep
|
|
51
|
+
"laneLinePrefab": "EmbLive/Prefabs/LiveGame/lane_line", # LiveLaneLineView._linePrefab (inner lines)
|
|
52
|
+
}
|
|
53
|
+
# Animator each AnimationTrack of the start timeline plays on (LiveStartTimelineDirector.BindTimelineTrack, by
|
|
54
|
+
# track name): a field of the scene's LiveStartTimelineDirector, or for LiveGameLaneEffect the timeline prefab's
|
|
55
|
+
# LivePlayableTimeline._laneInEffectAnimator. The SignalTrack goes to the prefab's SignalReceiver (no clips).
|
|
56
|
+
TIMELINE_TRACK_ANIMATORS = {
|
|
57
|
+
"UILiveStartCanvasTrack": ("scene", "LiveStartTimelineDirector", "_uiLiveStartCanvasAnimator"),
|
|
58
|
+
"LiveGameViewTrack": ("scene", "LiveStartTimelineDirector", "_liveGameViewAnimator"),
|
|
59
|
+
"LiveBackgroundViewTrack": ("scene", "LiveStartTimelineDirector", "_liveBackgroundViewAnimator"),
|
|
60
|
+
"LiveCamerasTrack": ("scene", "LiveStartTimelineDirector", "_liveCameras"),
|
|
61
|
+
"MovingLightTrack": ("scene", "LiveStartTimelineDirector", "_movingLightAnimator"),
|
|
62
|
+
"LightWeightBackgroundStageTrack": ("scene", "LiveStartTimelineDirector", "_lightWeightBackgroundStageAnimator"),
|
|
63
|
+
"LiveGameLaneEffect": ("timeline", "LivePlayableTimeline", "_laneInEffectAnimator"),
|
|
64
|
+
}
|
|
65
|
+
# sprite keys (a .png key's Sprite sub-asset): LightWeight composite inputs (LiveViewPresenter)
|
|
66
|
+
SPRITE_KEYS = {
|
|
67
|
+
"lightweightBackground": "Band/{band}/live_stage/lightweight_background",
|
|
68
|
+
"jacket": "Image/Jacket/{jacket}",
|
|
69
|
+
}
|
|
70
|
+
# Scene nodes whose textures the LightWeight preview draws (Live/RenderCanvas images);
|
|
71
|
+
# every other texture of the scene prefab belongs to UI the preview does not show (headers, gauges, pause, gekisou,
|
|
72
|
+
# skill cut-in, finish direction, mode-1 background) and is not written: its record stays, with "exported": false.
|
|
73
|
+
SCENE_DRAWN_TEXTURE_NODES = ("Live/RenderCanvas/LightWeightBackgroundShadowImage",)
|
|
74
|
+
# shaders the live screen finds by name (GraphicsSettings always-included, APK player data):
|
|
75
|
+
# LightWeight composite and blur, MV/VJ video
|
|
76
|
+
PLAYER_SHADERS = ("Hidden/CompositeRectBlit", "Hidden/SimpleGaussianBlur", "Sirius/Live/LiveSofdecPrimeYuv",
|
|
77
|
+
"Sirius/Live/LiveSofdecPrimeYuv_Android", "UI/Default")
|
|
78
|
+
# URP renderer indices used by the scene's live cameras
|
|
79
|
+
LIVE_RENDERERS = (2, 3, 4, 5)
|
|
80
|
+
# option items the render path reads
|
|
81
|
+
OPTION_ITEMS = {k: OPTION_ITEM_TYPES[k] for k in (3, 6, 104, 105, 106, 107, 108, 109, 110, 115, 200, 201, 202,
|
|
82
|
+
203, 300, 301, 302, 305, 306, 307, 308, 600)}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def master_rows(master: Path, music_id: int) -> dict:
|
|
86
|
+
music = next(r for r in master_table(master, "MasterLiveMusic") if r["_id"] == music_id)
|
|
87
|
+
opts = {r["_optionItemType"]: r["_valueString"] for r in master_table(master, "MasterOptionDefault")
|
|
88
|
+
if r["_presetId"] == 1 and r["_optionItemType"] in OPTION_ITEMS}
|
|
89
|
+
return {
|
|
90
|
+
"liveMusic": music,
|
|
91
|
+
"liveQualitySettings": master_table(master, "MasterLiveQualitySettings"),
|
|
92
|
+
"liveLaneSkin": master_table(master, "MasterLiveLaneSkin"),
|
|
93
|
+
"liveStageVideo": master_table(master, "MasterLiveStageVideo"),
|
|
94
|
+
"optionDefaultsPreset1": {OPTION_ITEMS[k]: {"id": k, "value": v} for k, v in sorted(opts.items())},
|
|
95
|
+
"optionRanges": {OPTION_ITEMS[r["_optionItemType"]]: [r["_minValue"], r["_maxValue"]]
|
|
96
|
+
for r in master_table(master, "MasterOptionRange") if r["_optionItemType"] in OPTION_ITEMS},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- derived lane geometry (mirrors the game functions named per entry) ---
|
|
101
|
+
# LiveLaneLineView's constant tables: split border main / sub counts per LiveLaneSplitCountType
|
|
102
|
+
# (None, Lane4, Lane6, Lane8, Lane12)
|
|
103
|
+
SPLIT_MAIN = (0, 4, 6, 8, 12)
|
|
104
|
+
SPLIT_SUB = (12, 12, 12, 0, 0)
|
|
105
|
+
LANE_COUNT = 24 # LiveDataCreator.CreateBootData passes 0x18
|
|
106
|
+
# LiveBootDataCreator.CreateViewData: arguments of LiveSettingCreator.CreateLaneViewSettings
|
|
107
|
+
LANE_VIEW = {"size": (1920.0, 1080.0), "topRange": 0.05, "bottomRange": 1.0,
|
|
108
|
+
"judgementScreenBottomPosition": 2.24, "topPosition": 0.0}
|
|
109
|
+
REF_ASPECT = 1.7777778 # LiveGameView.FullInitialize
|
|
110
|
+
REF_FOV = 54.0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def vertical_fov(width: int, height: int) -> float:
|
|
114
|
+
"""LiveGameView.CalcVerticalFovKeepingHorizontal, aspect clamped to <= 16:9 by the caller."""
|
|
115
|
+
a = min(width / height, REF_ASPECT)
|
|
116
|
+
h = math.atan(math.tan(math.radians(REF_FOV) * 0.5) * REF_ASPECT)
|
|
117
|
+
return math.degrees(2 * math.atan(math.tan(h) / a))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _node(nodes: list, path: str) -> dict:
|
|
121
|
+
hit = [n for n in nodes if n["path"] == path]
|
|
122
|
+
if len(hit) != 1:
|
|
123
|
+
raise KeyError(f"{path}: {len(hit)} nodes")
|
|
124
|
+
return hit[0]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _comp(node: dict, cls: str) -> dict:
|
|
128
|
+
hit = [c for c in node["components"] if c.get("class", c["type"]) == cls]
|
|
129
|
+
if len(hit) != 1:
|
|
130
|
+
raise KeyError(f"{node['path']}: {len(hit)} {cls}")
|
|
131
|
+
return hit[0]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def derive_lane(nodes: list, options: dict) -> dict:
|
|
135
|
+
"""Lane geometry for the given option values (MasterOptionDefault preset 1 strings)."""
|
|
136
|
+
lv = _comp(_node(nodes, "LiveGameView/root/LiveGameLane"), "LiveLaneView")
|
|
137
|
+
llv = _comp(_node(nodes, "LiveGameView/root/LiveGameLane/lines"), "LiveLaneLineView")
|
|
138
|
+
jr = _node(nodes, "LiveGameView/root/LiveGameLane/judgement_root")
|
|
139
|
+
n = LANE_COUNT
|
|
140
|
+
width = float(np.float32(lv["_laneWidth"])) # double field, passed on as (float)
|
|
141
|
+
length = lv["_lineLength"]
|
|
142
|
+
unit = width / n
|
|
143
|
+
judge_z = jr["localPosition"]["z"] # parents are identity: world z of position 0
|
|
144
|
+
split = int(options["GuidelineCount"])
|
|
145
|
+
main, sub = SPLIT_MAIN[split], SPLIT_SUB[split]
|
|
146
|
+
settings = {s["LineType"]: s for s in llv["_settings"]}
|
|
147
|
+
lines = []
|
|
148
|
+
for i in range(n + 1): # LiveLaneLineView.UpdateProperties
|
|
149
|
+
if i == 0 or i == n:
|
|
150
|
+
t = 1 # GetSettings: OutSide
|
|
151
|
+
elif main and i % (n // main) == 0:
|
|
152
|
+
t = 0 # Normal
|
|
153
|
+
elif sub and i % (n // sub) == 0:
|
|
154
|
+
t = 2 # Space
|
|
155
|
+
else:
|
|
156
|
+
lines.append({"index": i, "active": False})
|
|
157
|
+
continue
|
|
158
|
+
st = settings[t]
|
|
159
|
+
x = -width * 0.5 + i * unit
|
|
160
|
+
if t == 2:
|
|
161
|
+
ln, z0 = llv["_spaceLineLength"], judge_z - llv["_spaceLineLength"] * 0.5
|
|
162
|
+
else:
|
|
163
|
+
ln, z0 = length, 0.0
|
|
164
|
+
to_w = st["LineWidthTo"] * 2 if (i == n // 2 and t != 2) else st["LineWidthTo"]
|
|
165
|
+
# LiveLaneLine.SetLine: _anchor 2 (left side line) x -= startWidth/2, _anchor 1 (right) x += startWidth/2
|
|
166
|
+
if i == 0:
|
|
167
|
+
x -= st["LineWidthFrom"] * 0.5
|
|
168
|
+
elif i == n:
|
|
169
|
+
x += st["LineWidthFrom"] * 0.5
|
|
170
|
+
lines.append({"index": i, "active": True, "type": ("Normal", "OutSide", "Space")[t],
|
|
171
|
+
"start": [x, 0.0, z0], "end": [x, 0.0, z0 + ln],
|
|
172
|
+
"startWidth": st["LineWidthFrom"], "endWidth": to_w,
|
|
173
|
+
"gradient": st["LineColor"], "alphaControlled": t != 1})
|
|
174
|
+
return {
|
|
175
|
+
"laneCount": n, "laneWidth": width, "unitWidth": unit, "lineLength": length,
|
|
176
|
+
"judgementZ": judge_z,
|
|
177
|
+
"unitCentersX": [unit * (i + 0.5) - width * 0.5 for i in range(n)],
|
|
178
|
+
"spawnRoots": [[unit * (i + 0.5) - width * 0.5, 0.0, length] for i in range(n)],
|
|
179
|
+
"judgementPositions": [[unit * (i + 0.5) - width * 0.5, 0.0, judge_z] for i in range(n)],
|
|
180
|
+
"splitCountType": split, "splitBorderMainCount": main, "splitBorderSubCount": sub,
|
|
181
|
+
"lines": lines,
|
|
182
|
+
"lineAlpha": min(max(int(options["GuidelineOpacity"]) / 100.0, 0.0), 1.0),
|
|
183
|
+
"laneBaseAlpha": min(max(int(options["LaneOpacity"]) / 100.0, 0.0), 1.0),
|
|
184
|
+
"showJudgementLine": options["JudgePositionDisplay"].upper() == "TRUE",
|
|
185
|
+
"laneJudgementPosOffset": (int(options["JudgePosition"]) + 5) / -10.0 + 1.0,
|
|
186
|
+
"screenPlane": lane_2d(),
|
|
187
|
+
"sources": {
|
|
188
|
+
"lines": "LiveLaneLineView.UpdateProperties, GetSettings, LiveLaneLine.SetLine",
|
|
189
|
+
"spawnRoots": "LiveNoteSpawnRoot.UpdateProperties",
|
|
190
|
+
"judgementPositions": "LiveNoteJudgementRoot3D.UpdateProperties",
|
|
191
|
+
"options": "LiveBootDataCreator.CreateViewData",
|
|
192
|
+
"screenPlane": "LiveSettingCreator.CreateLaneViewSettings, JudgementPositionLogic.GetJudgementPositions, LiveViewUtility.GetNoteSpawnPosition",
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def lane_2d(lane_count: int = LANE_COUNT) -> dict:
|
|
198
|
+
"""LiveSettingCreator.CreateLaneViewSettings as CreateViewData calls it (LANE_VIEW: size 1920x1080,
|
|
199
|
+
topRange 0.05, bottomRange 1, judgementScreenBottomPosition 2.24, topPosition 0) and
|
|
200
|
+
JudgementPositionLogic.GetJudgementPositions; units of 100 reference pixels."""
|
|
201
|
+
w, h = LANE_VIEW["size"][0] / 100.0, LANE_VIEW["size"][1] / 100.0
|
|
202
|
+
top, bottom = LANE_VIEW["topRange"], LANE_VIEW["bottomRange"]
|
|
203
|
+
lt, rt = [-w * top * 0.5, h * 0.5, 0.0], [w * top * 0.5, h * 0.5, 0.0]
|
|
204
|
+
lb, rb = [-w * bottom * 0.5, -h * 0.5, 0.0], [w * bottom * 0.5, -h * 0.5, 0.0]
|
|
205
|
+
t = min(max(LANE_VIEW["judgementScreenBottomPosition"] / (lt[1] - lb[1]), 0.0), 1.0)
|
|
206
|
+
jp = []
|
|
207
|
+
for i in range(lane_count):
|
|
208
|
+
f = min(max(i / lane_count, 0.0), 1.0)
|
|
209
|
+
xb = (rb[0] - lb[0]) / lane_count * 0.5 + lb[0] + (rb[0] - lb[0]) * f
|
|
210
|
+
xt = (rt[0] - lt[0]) / lane_count * 0.5 + lt[0] + (rt[0] - lt[0]) * f
|
|
211
|
+
yb, yt = lb[1] + (rb[1] - lb[1]) * f, lt[1] + (rt[1] - lt[1]) * f
|
|
212
|
+
jp.append([xb + t * (xt - xb), yb + (yt - yb) * t, 0.0])
|
|
213
|
+
# LiveViewUtility.GetNoteSpawnPosition: intersection of the two side edges
|
|
214
|
+
s1 = (lb[1] - lt[1]) / (lb[0] - lt[0])
|
|
215
|
+
s2 = (rb[1] - rt[1]) / (rb[0] - rt[0])
|
|
216
|
+
c1 = lt[1] - lt[0] * s1
|
|
217
|
+
sx = ((rt[1] - rt[0] * s2) - c1) / (s1 - s2)
|
|
218
|
+
return {"vertices": [lt, rt, lb, rb], "judgementPositions": jp, "spawnPosition": [sx, c1 + s1 * sx],
|
|
219
|
+
"pixelsPerUnit": 100.0}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# --- extraction -------------------------------------------------------------
|
|
223
|
+
def _component(nodes: list, cls: str) -> dict:
|
|
224
|
+
hit = [c for n in nodes for c in n["components"] if c.get("class", c["type"]) == cls]
|
|
225
|
+
if len(hit) != 1:
|
|
226
|
+
raise KeyError(f"{len(hit)} {cls} components")
|
|
227
|
+
return hit[0]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def bind_timeline(ex: Exporter, doc: dict, key: str) -> dict:
|
|
231
|
+
"""Bind the clips of the start timeline's tracks to the Animators the game binds them to
|
|
232
|
+
(TIMELINE_TRACK_ANIMATORS): binding paths resolve against that GameObject's subtree, and each track records
|
|
233
|
+
`boundAnimator` {key, gameObject, field}. Returns binding counts per track."""
|
|
234
|
+
tl = doc["assets"]["startTimeline"]
|
|
235
|
+
env, tl_graph = ex.load(key)
|
|
236
|
+
scene_graph = ex.load(SCENE_KEY)[1]
|
|
237
|
+
dir_json = _component(tl["nodes"], "PlayableDirector")
|
|
238
|
+
dir_path = next(n["path"] for n in tl["nodes"] if any(c is dir_json for c in n["components"]))
|
|
239
|
+
directors = [o for o in env.objects if o.type.name == "PlayableDirector"
|
|
240
|
+
and tl_graph.path(tl_graph.tf_of_go[o.read_typetree()["m_GameObject"]["m_PathID"]]) == dir_path]
|
|
241
|
+
if len(directors) != 1:
|
|
242
|
+
raise RuntimeError(f"{key}: {len(directors)} PlayableDirectors at {dir_path}")
|
|
243
|
+
asset_o = ex.deref(directors[0], directors[0].read_typetree()["m_PlayableAsset"])
|
|
244
|
+
tracks_json = {t["m_Name"]: t for t in dir_json["m_PlayableAsset"]["m_Tracks"]}
|
|
245
|
+
counts = {}
|
|
246
|
+
for _track_o, tt, clips in ex.timeline_tracks(asset_o):
|
|
247
|
+
name = tt["m_Name"]
|
|
248
|
+
if name not in TIMELINE_TRACK_ANIMATORS:
|
|
249
|
+
if clips:
|
|
250
|
+
raise RuntimeError(f"{key}: track {name} with clips has no known binding")
|
|
251
|
+
continue
|
|
252
|
+
where, cls, field = TIMELINE_TRACK_ANIMATORS[name]
|
|
253
|
+
nodes, graph, owner = ((doc["scene"]["nodes"], scene_graph, SCENE_KEY) if where == "scene"
|
|
254
|
+
else (tl["nodes"], tl_graph, key))
|
|
255
|
+
ref = _component(nodes, cls)[field]
|
|
256
|
+
if not ref or ref.get("component") != "Animator":
|
|
257
|
+
raise RuntimeError(f"{cls}.{field}: {ref}")
|
|
258
|
+
root = ref["gameObject"]
|
|
259
|
+
tracks_json[name]["boundAnimator"] = {"key": owner, "gameObject": root, "field": f"{cls}.{field}"}
|
|
260
|
+
counts[name] = [ex.bind_clip(c, graph, root) for c in clips]
|
|
261
|
+
return counts
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def lane_skin(rows: dict) -> str:
|
|
265
|
+
"""Lane skin asset name: option LiveSkinId (305) -> MasterLiveLaneSkin row of that id (LaneSkinLoadStep)."""
|
|
266
|
+
sid = int(rows["optionDefaultsPreset1"]["LiveSkinId"]["value"])
|
|
267
|
+
hit = [r for r in rows["liveLaneSkin"] if r["_id"] == sid]
|
|
268
|
+
if len(hit) != 1:
|
|
269
|
+
raise KeyError(f"MasterLiveLaneSkin: {len(hit)} rows with id {sid} (option LiveSkinId)")
|
|
270
|
+
return hit[0]["_assetName"]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _texture_records(v, out: list) -> None:
|
|
274
|
+
if isinstance(v, dict):
|
|
275
|
+
if isinstance(v.get("texture"), str) and v["texture"].startswith("textures/"):
|
|
276
|
+
out.append(v)
|
|
277
|
+
for x in v.values():
|
|
278
|
+
_texture_records(x, out)
|
|
279
|
+
elif isinstance(v, list):
|
|
280
|
+
for x in v:
|
|
281
|
+
_texture_records(x, out)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _drop_scene_textures(doc: dict, base: Path, unlink: bool = True) -> list[str]:
|
|
285
|
+
"""The texture files only the undrawn part of the scene prefab references (SCENE_DRAWN_TEXTURE_NODES): their
|
|
286
|
+
records marked "exported": false, the files deleted (`unlink`; else the caller does not write them). Returns
|
|
287
|
+
the files."""
|
|
288
|
+
keep: list = []
|
|
289
|
+
for k, v in doc.items():
|
|
290
|
+
if k != "scene":
|
|
291
|
+
_texture_records(v, keep)
|
|
292
|
+
for n in doc["scene"]["nodes"]:
|
|
293
|
+
if n["path"] in SCENE_DRAWN_TEXTURE_NODES:
|
|
294
|
+
_texture_records(n["components"], keep)
|
|
295
|
+
kept = {r["texture"] for r in keep}
|
|
296
|
+
scene: list = []
|
|
297
|
+
_texture_records(doc["scene"], scene)
|
|
298
|
+
dropped = sorted({r["texture"] for r in scene} - kept)
|
|
299
|
+
for r in scene:
|
|
300
|
+
if r["texture"] in dropped:
|
|
301
|
+
r["exported"] = False
|
|
302
|
+
if unlink:
|
|
303
|
+
for rel in dropped:
|
|
304
|
+
(base / rel).unlink()
|
|
305
|
+
return dropped
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class SharedSceneMismatch(RuntimeError):
|
|
309
|
+
"""A music's scene cannot be composed from its band's part (see music_part); export it whole."""
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _export(cat, player, base: Path, rows: dict | None, band: int, skin: str, extra_keys: dict | None,
|
|
313
|
+
slice_: dict | None, jacket: str | None):
|
|
314
|
+
"""The export sequence of extract (jacket None: without the jacket sprite, the slice and the music rows left as
|
|
315
|
+
placeholders for music_part). -> (exporter, doc, shader index, shader summary, timeline counts, dropped)."""
|
|
316
|
+
ex = Exporter(cat, base, player=player, clip_format="livescene", defer_writes=True)
|
|
317
|
+
graphics = player.graphics()
|
|
318
|
+
doc: dict = {"player": graphics}
|
|
319
|
+
doc["slice"] = slice_
|
|
320
|
+
doc["scene"] = ex.prefab(SCENE_KEY)
|
|
321
|
+
doc["laneSkin"] = {part: ex.key_sprite(f"Live/Lane/{skin}/{part}") for part in LANE_SKIN_PARTS}
|
|
322
|
+
keys = dict(ASSET_KEYS)
|
|
323
|
+
keys.update(extra_keys or {})
|
|
324
|
+
doc["assets"] = {name: ex.export_key(key.format(band=band)) for name, key in keys.items()}
|
|
325
|
+
timeline = bind_timeline(ex, doc, keys["startTimeline"].format(band=band))
|
|
326
|
+
doc["sprites"] = {name: ex.key_sprite(key.format(band=band, jacket=jacket))
|
|
327
|
+
for name, key in SPRITE_KEYS.items() if jacket is not None or name != "jacket"}
|
|
328
|
+
if rows:
|
|
329
|
+
doc["master"] = rows if jacket is not None else None
|
|
330
|
+
opts = {k: v["value"] for k, v in rows["optionDefaultsPreset1"].items()}
|
|
331
|
+
doc["derived"] = {
|
|
332
|
+
"lane": derive_lane(doc["scene"]["nodes"], opts),
|
|
333
|
+
"fovRule": {"refFov": REF_FOV, "refAspect": REF_ASPECT,
|
|
334
|
+
"source": "LiveGameView.FullInitialize / CalcVerticalFovKeepingHorizontal",
|
|
335
|
+
"examples": {f"{w}x{h}": vertical_fov(w, h)
|
|
336
|
+
for w, h in ((2340, 1080), (1920, 1080), (1440, 1080), (2048, 1536))}}}
|
|
337
|
+
for name in PLAYER_SHADERS:
|
|
338
|
+
ex.add_shader(player.shader(name))
|
|
339
|
+
rl = graphics["pipelines"][graphics["defaultPipeline"]]["m_RendererDataList"]
|
|
340
|
+
renderers = [rl[i] for i in LIVE_RENDERERS]
|
|
341
|
+
for rn in renderers:
|
|
342
|
+
for o in player.renderer_shaders(rn):
|
|
343
|
+
ex.add_shader(o)
|
|
344
|
+
doc["postTextures"] = {}
|
|
345
|
+
for rn in renderers:
|
|
346
|
+
if graphics["renderers"][rn].get("postProcessData"):
|
|
347
|
+
doc["postTextures"][rn] = {"filmGrainTex": [ex.texture(o) for o in player.post_textures(rn, "filmGrainTex")]}
|
|
348
|
+
index, summary = ex.dump_shaders(base / "shaders")
|
|
349
|
+
doc["shaders"] = {"index": "shaders/shaders.json", "names": summary["names"], "cameraRenderers": renderers}
|
|
350
|
+
doc["convention"] = ("Unity space (left-handed, Y up); transforms are local TRS; "
|
|
351
|
+
"quaternions (x,y,z,w); colours in the project's Gamma space")
|
|
352
|
+
ex.resolve_pending() # bindings whose target hierarchy / component class was exported after the clip
|
|
353
|
+
dropped = _drop_scene_textures(doc, base, unlink=False)
|
|
354
|
+
ex.write_textures(skip=dropped)
|
|
355
|
+
return ex, doc, index, summary, timeline, dropped
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _summary(doc: dict, index: list, summary: dict, textures: int, dropped: list, timeline) -> dict:
|
|
359
|
+
gles3 = sum(1 for r in index for v in r["variants"] if v["platform"] == "gles3")
|
|
360
|
+
return {"scene": "livescene/scene.json", "nodes": len(doc["scene"]["nodes"]),
|
|
361
|
+
"assets": list(doc["assets"]), "textures": textures - len(dropped), "droppedTextures": len(dropped),
|
|
362
|
+
"shaders": summary["count"], "variants": summary["variants"], "gles3Variants": gles3,
|
|
363
|
+
"timelineBindings": timeline}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def extract(cat, player, out_dir: Path, master: Path | None = None, music_id: int = 100001,
|
|
367
|
+
band: int | None = None, band_choice: dict | None = None, extra_keys: dict | None = None) -> dict:
|
|
368
|
+
"""Write <out_dir>/livescene/scene.json (+ textures/, shaders/) and return a summary.
|
|
369
|
+
|
|
370
|
+
`band`: the band of the LightWeight background (LightWeightBackgroundLoadStep.<LoadAsync>d__3:
|
|
371
|
+
SelfMemberList[2].BandID) and of the start timeline (LiveResourceBandResolver.Resolve). The game
|
|
372
|
+
takes it from the player's deck; the caller chooses it (live.resolve_band) and `band_choice` records how.
|
|
373
|
+
`master` (decoded master JSON dir) adds the music/option rows and the derived lane geometry; with `master`
|
|
374
|
+
the band is required. Without `master`: band 1 unless given, lane skin skin001."""
|
|
375
|
+
out_dir = Path(out_dir)
|
|
376
|
+
rows = master_rows(master, music_id) if master else None
|
|
377
|
+
if band is None:
|
|
378
|
+
if rows:
|
|
379
|
+
raise ValueError(f"music {music_id}: no band given; the LightWeight background and the start timeline "
|
|
380
|
+
"take the band of the deck centre (SelfMemberList[2]), see live.resolve_band")
|
|
381
|
+
band = 1
|
|
382
|
+
skin = lane_skin(rows) if rows else "skin001"
|
|
383
|
+
base = out_dir / "livescene"
|
|
384
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
385
|
+
slice_ = {"musicId": music_id, "band": band, "laneSkin": skin}
|
|
386
|
+
if band_choice:
|
|
387
|
+
slice_["bandChoice"] = band_choice
|
|
388
|
+
jacket = rows["liveMusic"]["_jacketAssetName"] if rows else "jkt_001_100001"
|
|
389
|
+
ex, doc, index, summary, timeline, dropped = _export(cat, player, base, rows, band, skin, extra_keys, slice_,
|
|
390
|
+
jacket)
|
|
391
|
+
write_json(base / "scene.json", doc)
|
|
392
|
+
return _summary(doc, index, summary, len(ex.textures), dropped, timeline)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
BAND_DOC = "band.json"
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def band_part(cat, player, out_dir: Path, master: Path, music_id: int, band: int,
|
|
399
|
+
extra_keys: dict | None = None) -> dict:
|
|
400
|
+
"""What `extract` writes for any music of `band`: <out_dir>/livescene/ (textures, shaders; no scene.json) and
|
|
401
|
+
<out_dir>/band.json (the document with the slice, the jacket sprite and the music rows left out). `music_id`:
|
|
402
|
+
any music of the band (the option rows it reads are the same for every music)."""
|
|
403
|
+
out_dir = Path(out_dir)
|
|
404
|
+
rows = master_rows(master, music_id)
|
|
405
|
+
base = out_dir / "livescene"
|
|
406
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
407
|
+
ex, doc, index, summary, timeline, dropped = _export(cat, player, base, rows, band, lane_skin(rows),
|
|
408
|
+
extra_keys, None, None)
|
|
409
|
+
part = {"band": band, "doc": doc, "summary": _summary(doc, index, summary, len(ex.textures), dropped, timeline),
|
|
410
|
+
"textures": sorted({r["texture"] for r in _records(doc["scene"])})}
|
|
411
|
+
write_json(out_dir / BAND_DOC, part)
|
|
412
|
+
return part
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _records(v) -> list:
|
|
416
|
+
out: list = []
|
|
417
|
+
_texture_records(v, out)
|
|
418
|
+
return out
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def music_part(cat, player, out_dir: Path, part: dict, master: Path, music_id: int, band_choice: dict | None = None
|
|
422
|
+
) -> dict:
|
|
423
|
+
"""<out_dir>/livescene/scene.json of one music from its band's part (band_part; the part's livescene/ files
|
|
424
|
+
already in <out_dir>/livescene): the document extract writes. Raises SharedSceneMismatch when the jacket's
|
|
425
|
+
bundle closure holds shaders or a hierarchy (extract would dump or resolve against them) or the jacket texture
|
|
426
|
+
is one of the scene's (extract would keep it)."""
|
|
427
|
+
from .export import Exporter as _Exporter
|
|
428
|
+
out_dir = Path(out_dir)
|
|
429
|
+
band = part["band"]
|
|
430
|
+
rows = master_rows(master, music_id)
|
|
431
|
+
doc = json.loads(json.dumps(part["doc"]))
|
|
432
|
+
key = SPRITE_KEYS["jacket"].format(band=band, jacket=rows["liveMusic"]["_jacketAssetName"])
|
|
433
|
+
ex = _Exporter(cat, out_dir / "livescene", player=player, clip_format="livescene")
|
|
434
|
+
env, _ = ex.load(key)
|
|
435
|
+
kinds = {o.type.name for o in env.objects} & {"Shader", "GameObject", "Transform", "RectTransform"}
|
|
436
|
+
if kinds:
|
|
437
|
+
raise SharedSceneMismatch(f"{key}: its closure holds {', '.join(sorted(kinds))}")
|
|
438
|
+
jacket = ex.key_sprite(key)
|
|
439
|
+
if {r["texture"] for r in _records(jacket)} & set(part["textures"]):
|
|
440
|
+
raise SharedSceneMismatch(f"{key}: its texture is one of the scene's")
|
|
441
|
+
slice_ = {"musicId": music_id, "band": band, "laneSkin": lane_skin(rows)}
|
|
442
|
+
if band_choice:
|
|
443
|
+
slice_["bandChoice"] = band_choice
|
|
444
|
+
doc["slice"] = slice_
|
|
445
|
+
doc["sprites"]["jacket"] = jacket
|
|
446
|
+
doc["master"] = rows
|
|
447
|
+
write_json(out_dir / "livescene" / "scene.json", doc)
|
|
448
|
+
s = dict(part["summary"])
|
|
449
|
+
s["textures"] += len(ex.textures)
|
|
450
|
+
return s
|