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.
Files changed (79) hide show
  1. nnnotes/__init__.py +7 -0
  2. nnnotes/__main__.py +4 -0
  3. nnnotes/acb.py +190 -0
  4. nnnotes/addressables.py +413 -0
  5. nnnotes/adv.py +243 -0
  6. nnnotes/advcommand.py +29 -0
  7. nnnotes/advmedia.py +178 -0
  8. nnnotes/advscene.py +113 -0
  9. nnnotes/advui.py +1154 -0
  10. nnnotes/advvideo.py +684 -0
  11. nnnotes/atoms/__init__.py +151 -0
  12. nnnotes/atoms/astc.py +52 -0
  13. nnnotes/atoms/gltf.py +290 -0
  14. nnnotes/atoms/mesh.py +289 -0
  15. nnnotes/atoms/png.py +28 -0
  16. nnnotes/atoms/reader.py +269 -0
  17. nnnotes/atoms/sniff.py +198 -0
  18. nnnotes/atoms/sprite.py +236 -0
  19. nnnotes/atoms/texture.py +80 -0
  20. nnnotes/cache.py +289 -0
  21. nnnotes/catalog.py +373 -0
  22. nnnotes/catalogdb.py +457 -0
  23. nnnotes/census.py +276 -0
  24. nnnotes/cli.py +744 -0
  25. nnnotes/cli_assets.py +1539 -0
  26. nnnotes/config.py +238 -0
  27. nnnotes/contract.py +497 -0
  28. nnnotes/cri.py +371 -0
  29. nnnotes/crikey.py +71 -0
  30. nnnotes/crilips.py +265 -0
  31. nnnotes/cristages.py +558 -0
  32. nnnotes/deckdata.py +462 -0
  33. nnnotes/export.py +1462 -0
  34. nnnotes/gameapi.py +318 -0
  35. nnnotes/jsonio.py +98 -0
  36. nnnotes/languages.py +44 -0
  37. nnnotes/layout.py +596 -0
  38. nnnotes/link.py +418 -0
  39. nnnotes/live.py +115 -0
  40. nnnotes/live2d.py +235 -0
  41. nnnotes/liveaudio.py +331 -0
  42. nnnotes/livenotes.py +291 -0
  43. nnnotes/liveoptions.py +250 -0
  44. nnnotes/livescene.py +450 -0
  45. nnnotes/liveui.py +328 -0
  46. nnnotes/master.py +251 -0
  47. nnnotes/modelstages.py +424 -0
  48. nnnotes/motion.py +131 -0
  49. nnnotes/objexport.py +1564 -0
  50. nnnotes/orchestrate.py +630 -0
  51. nnnotes/plan.py +218 -0
  52. nnnotes/player.py +392 -0
  53. nnnotes/room.py +391 -0
  54. nnnotes/score.py +1139 -0
  55. nnnotes/shader.py +217 -0
  56. nnnotes/spot.py +201 -0
  57. nnnotes/stages.py +285 -0
  58. nnnotes/store.py +518 -0
  59. nnnotes/story.py +142 -0
  60. nnnotes/storyfonts.py +1258 -0
  61. nnnotes/storyhost.py +1190 -0
  62. nnnotes/storysite.py +662 -0
  63. nnnotes/storyvoices.py +102 -0
  64. nnnotes/textstyle.py +355 -0
  65. nnnotes/tmpfont.py +636 -0
  66. nnnotes/typetrees/6000.3.12f1.json +19 -0
  67. nnnotes/unity.py +368 -0
  68. nnnotes/viewrules.json +441 -0
  69. nnnotes/views.py +966 -0
  70. nnnotes/voices.py +1363 -0
  71. nnnotes/web.py +1624 -0
  72. nnnotes/webaudio.py +14 -0
  73. nnnotes/webmodel.py +397 -0
  74. nnnotes-0.1.0.dist-info/METADATA +200 -0
  75. nnnotes-0.1.0.dist-info/RECORD +79 -0
  76. nnnotes-0.1.0.dist-info/WHEEL +5 -0
  77. nnnotes-0.1.0.dist-info/entry_points.txt +2 -0
  78. nnnotes-0.1.0.dist-info/licenses/LICENSE +21 -0
  79. 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