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/livenotes.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Live note / effect runtime data -> livenotes/notes.json + textures + shaders.
|
|
2
|
+
|
|
3
|
+
Everything spawned or animated per note in the in-play note highway:
|
|
4
|
+
note view prefabs (tap, flick +left/right/direction,
|
|
5
|
+
trace, slide/slide_end/connection, guide, slide_combo(+skip), none), long-note
|
|
6
|
+
body prefabs (slide_line_view, guide_line_view), the pair line prefab,
|
|
7
|
+
the note skin asset (sprites per note type/width/tilt), the note-effect skin settings
|
|
8
|
+
of the NoteEffectId option and the lane-effect settings (set effect001) with every effect
|
|
9
|
+
prefab they reference (particle systems exported with all module data), the judgement text view and
|
|
10
|
+
the judgement / combo sprite assets, the combo animator, the note animation controllers
|
|
11
|
+
and clips they reference, and the option / master values the motion formulas read.
|
|
12
|
+
Only what the default options load: the effect set of NoteEffectId (307) at quality Middle
|
|
13
|
+
(the `<name>Light` set is Low only, `NoteSkinLoadStep.GetLightQualityAddress`; the
|
|
14
|
+
`<name>Simple` sets are not used with default options); no bar / skill line prefabs
|
|
15
|
+
(MeasureLineDisplay 109 / LiveSkillActivationPositionDisplay 309 FALSE). With liveoptions.LiveOptions
|
|
16
|
+
also: the other offered note skins (`noteSkins`, `settings.skins`), the other offered note effect sets
|
|
17
|
+
and, with LiveQuality Low offered, their `Light` variants where the catalog has them (in `assets`,
|
|
18
|
+
`settings.effects`), with MeasureLineDisplay offered the bar line view (`prefabs.bar_line_view`: the
|
|
19
|
+
LiveBarLineView prefab the live scene's LiveBarLineViewContainer instantiates, its _elementPrefab), and the
|
|
20
|
+
option tables (`settings.optionDefaults` / `optionRanges` of every item the player offers).
|
|
21
|
+
|
|
22
|
+
Objects go through export.Exporter in its "livenotes" clip format (clips and animator
|
|
23
|
+
controllers in registries referenced by id; binding paths and attributes left as
|
|
24
|
+
"crc32:<n>" until a pass names them), following references to AnimatorControllers
|
|
25
|
+
and SpriteAtlases. Clip binding paths that point into the Live scene's UI subtrees
|
|
26
|
+
are named from that scene (`add_path_source`), which is not exported here.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
|
|
34
|
+
from .catalog import Catalog
|
|
35
|
+
from .export import Exporter
|
|
36
|
+
from .jsonio import write_json
|
|
37
|
+
from .liveoptions import OPTION_ITEM_TYPES, PLAYER_ITEMS, LiveOptions
|
|
38
|
+
from .livescene import LANE_COUNT, LANE_VIEW
|
|
39
|
+
from .player import PlayerData
|
|
40
|
+
from .score import master_table
|
|
41
|
+
|
|
42
|
+
# --- keys ------------------------------------------------------------------
|
|
43
|
+
NOTE_VIEWS = ["tap", "flick", "flick_left", "flick_right", "trace", "slide", "slide_end",
|
|
44
|
+
"connection", "guide", "slide_combo", "slide_combo_skip", "none"]
|
|
45
|
+
PREFABS = {f"{v}_note_view": f"EmbLive/Prefabs/LiveGame/{v}_note_view" for v in NOTE_VIEWS}
|
|
46
|
+
PREFABS.update({
|
|
47
|
+
"slide_line_view": "EmbLive/Prefabs/LiveGame/slide_line_view",
|
|
48
|
+
"guide_line_view": "EmbLive/Prefabs/LiveGame/guide_line_view",
|
|
49
|
+
"pair_note_line": "EmbLive/Prefabs/LiveGame/PairNoteLine/LiveGamePairNoteView",
|
|
50
|
+
"note_groove_loop": "EmbLive/Prefabs/LiveGame/Effect/note_groove_loop",
|
|
51
|
+
"judge_effect_view": "EmbLive/Prefabs/UILiveNoteJudgeEffectView",
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
# --- defaults (fresh profile) -------------------------------------------------
|
|
55
|
+
# MasterOptionDefault preset 1 (CurrentPresetIndex 0 -> Preset1); item types from App.Options.OptionItemType.
|
|
56
|
+
OPTION_ITEMS = {k: OPTION_ITEM_TYPES[k] for k in (1, 4, 6, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109,
|
|
57
|
+
112, 113, 117, 119, 200, 205, 206, 207, 208, 300, 301, 302,
|
|
58
|
+
305, 306, 307, 308, 309, 310)}
|
|
59
|
+
LIVE_SETTING_KEYS = ["note_speed_min", "note_speed_max", "note_speed_view_min", "note_speed_view_max",
|
|
60
|
+
"note_slide_combo_rhythmic_unit", "stop_in_vain_time_ms_diff",
|
|
61
|
+
"slide_offset_min_note_lane_width", "slide_offset_max_note_lane_width",
|
|
62
|
+
"note_overlap_lane_buffer"]
|
|
63
|
+
DEFAULT_SKIN = "skin001" # MasterLiveNoteSkin._id 1 (NoteDesignId default 1)
|
|
64
|
+
DEFAULT_EFFECT = "effect001" # MasterLiveNoteEffectSkin._id 1 (NoteEffectId default 1)
|
|
65
|
+
|
|
66
|
+
# Catalog key families exported whole (every key under the prefix): the note effect settings of the NoteEffectId
|
|
67
|
+
# option's set ({effect}; LiveAddressablePath.GetLiveNoteEffectSkinAssetPath) and the lane effect settings, whose
|
|
68
|
+
# set is always LANE_EFFECT (LaneEffectLoadStep.GetAssetPath passes that constant to
|
|
69
|
+
# LiveAddressablePath.GetLiveLaneEffectSkinAssetPath), with the effect prefabs they reference by GameObject
|
|
70
|
+
LANE_EFFECT = "effect001"
|
|
71
|
+
KEY_PREFIXES = [
|
|
72
|
+
"Effect/Live/NoteEffect/{effect}/",
|
|
73
|
+
f"Effect/Live/LaneEffect/{LANE_EFFECT}/",
|
|
74
|
+
]
|
|
75
|
+
SINGLE_KEYS = [
|
|
76
|
+
"EmbLive/Animation/Combo/UILiveCombo", # UIComboCounterView's animator controller
|
|
77
|
+
"Live/UiSpriteAssets/LiveComboSpriteAsset",
|
|
78
|
+
"Live/UiSpriteAssets/LiveJudgementSpriteAsset",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def settings(master: Path | None, options: LiveOptions = LiveOptions()) -> dict:
|
|
83
|
+
"""Option defaults and master values the note runtime reads; with `options` also the offered note skins
|
|
84
|
+
(`skins`: NoteDesignId -> MasterLiveNoteSkin._assetName) and effect sets (`effects`: NoteEffectId ->
|
|
85
|
+
MasterLiveNoteEffectSkin._assetName; also when only other qualities are offered, whose effect set name the
|
|
86
|
+
player derives from it), and with its option tables the items of PLAYER_ITEMS as well."""
|
|
87
|
+
out: dict = {"skin": DEFAULT_SKIN, "effect": DEFAULT_EFFECT, "laneCount": LANE_COUNT,
|
|
88
|
+
"laneSize": list(LANE_VIEW["size"]), "laneTopRange": LANE_VIEW["topRange"],
|
|
89
|
+
"laneBottomRange": LANE_VIEW["bottomRange"],
|
|
90
|
+
"judgementScreenBottomPosition": LANE_VIEW["judgementScreenBottomPosition"],
|
|
91
|
+
"laneTopPosition": LANE_VIEW["topPosition"],
|
|
92
|
+
"noteViewProgress": {"base": 1.065, "exponentScale": 45.0},
|
|
93
|
+
"tiltCenterLane": 11.5}
|
|
94
|
+
if master is None:
|
|
95
|
+
return out
|
|
96
|
+
items = OPTION_ITEMS
|
|
97
|
+
if options.tables:
|
|
98
|
+
items = {**OPTION_ITEMS, **{k: OPTION_ITEM_TYPES[k] for k in PLAYER_ITEMS}}
|
|
99
|
+
opts = {}
|
|
100
|
+
for r in master_table(master, "MasterOptionDefault"):
|
|
101
|
+
if r["_presetId"] == 1 and r["_optionItemType"] in items:
|
|
102
|
+
opts[items[r["_optionItemType"]]] = r["_valueString"]
|
|
103
|
+
out["optionDefaults"] = opts
|
|
104
|
+
out["optionRanges"] = {items.get(r["_optionItemType"], r["_optionItemType"]): [r["_minValue"], r["_maxValue"]]
|
|
105
|
+
for r in master_table(master, "MasterOptionRange") if r["_optionItemType"] in items}
|
|
106
|
+
ls = {r["_key"]: r["_value"] for r in master_table(master, "MasterLiveSettings")}
|
|
107
|
+
out["liveSettings"] = {k: ls[k] for k in LIVE_SETTING_KEYS if k in ls}
|
|
108
|
+
skins = {r["_id"]: r["_assetName"] for r in master_table(master, "MasterLiveNoteSkin")}
|
|
109
|
+
effects = {r["_id"]: r["_assetName"] for r in master_table(master, "MasterLiveNoteEffectSkin")}
|
|
110
|
+
out["skin"] = skins[int(opts["NoteDesignId"])]
|
|
111
|
+
out["effect"] = effects[int(opts["NoteEffectId"])]
|
|
112
|
+
out["judgementSprites"] = {r["_noteSimulateJudgement"]: r["_spriteName"]
|
|
113
|
+
for r in master_table(master, "MasterLiveJudgementSprite")}
|
|
114
|
+
out["noteDisplayTimeMs"] = display_time_ms(float(opts["NoteSpeed"]), float(ls["note_speed_min"]),
|
|
115
|
+
float(ls["note_speed_max"]), float(ls["note_speed_view_min"]),
|
|
116
|
+
float(ls["note_speed_view_max"]))
|
|
117
|
+
if options.designs:
|
|
118
|
+
out["skins"] = {str(i): skins[i] for i in options.designs}
|
|
119
|
+
if options.effects or options.qualities:
|
|
120
|
+
out["effects"] = {str(i): effects[i] for i in options.values("NoteEffectId")}
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def display_time_ms(speed: float, smin: float, smax: float, vmin: float, vmax: float) -> int:
|
|
125
|
+
"""NoteBeforePlayingTimeGetter.GetNoteDisplayOffsetTimeMs, in float32."""
|
|
126
|
+
f = np.float32
|
|
127
|
+
t = max(f(0), f(speed) - f(smin)) / (f(smax) - f(smin))
|
|
128
|
+
t = min(max(t, f(0)), f(1))
|
|
129
|
+
e = f(1) - f(np.power(f(1) - t, f(1.31)))
|
|
130
|
+
e = min(max(e, f(0)), f(1))
|
|
131
|
+
return int(f((f(vmin) + (f(vmax) - f(vmin)) * e) * f(1000)))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# UI subtrees of [EmbScene/Live] animated by exported clips (combo counter, judgement text); used only to name
|
|
135
|
+
# clip binding paths, the scene itself is exported by livescene.
|
|
136
|
+
SCENE_KEY = "EmbScene/Live"
|
|
137
|
+
SCENE_SUBTREES = ("UILiveCombo", "UILiveJudgement")
|
|
138
|
+
# The bar line view (MeasureLineDisplay): the scene's LiveAllNoteView -> _barLineView (LiveAllBarLineView) ->
|
|
139
|
+
# _container (LiveBarLineViewContainer) -> _elementPrefab (LiveBarLineView, the prefab the container instantiates)
|
|
140
|
+
NOTE_VIEW = ("LiveGameView/LiveGameCamera/screen_root/LiveGameAllNoteView", "LiveAllNoteView")
|
|
141
|
+
BAR_LINE_CHAIN = (("_barLineView", "LiveAllBarLineView"), ("_container", "LiveBarLineViewContainer"),
|
|
142
|
+
("_elementPrefab", "LiveBarLineView"))
|
|
143
|
+
BAR_LINE_VIEW = "bar_line_view"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
TMP_FONT_NOTE = ("not exported: the note highway draws no TextMeshPro text with default options (the judge view's "
|
|
147
|
+
"fast / slow and assist texts stay hidden)")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _drop_tmp_fonts(doc: dict, root: Path) -> list[str]:
|
|
151
|
+
"""Reduce the font asset and material of every TextMeshProUGUI record to a name stub and delete the texture
|
|
152
|
+
files no record references any more (the TMP font atlases). Returns the deleted files."""
|
|
153
|
+
def walk(v):
|
|
154
|
+
if isinstance(v, dict):
|
|
155
|
+
if v.get("class") == "TextMeshProUGUI" and "m_fontAsset" in v:
|
|
156
|
+
fa, mat = v["m_fontAsset"], v.get("m_sharedMaterial")
|
|
157
|
+
if isinstance(fa, dict):
|
|
158
|
+
v["m_fontAsset"] = {"asset": fa.get("asset"), "name": fa.get("name"), "note": TMP_FONT_NOTE}
|
|
159
|
+
if isinstance(mat, dict):
|
|
160
|
+
v["m_sharedMaterial"] = {"material": mat.get("material"), "shader": mat.get("shader"),
|
|
161
|
+
"note": TMP_FONT_NOTE}
|
|
162
|
+
for x in v.values():
|
|
163
|
+
walk(x)
|
|
164
|
+
elif isinstance(v, list):
|
|
165
|
+
for x in v:
|
|
166
|
+
walk(x)
|
|
167
|
+
walk(doc)
|
|
168
|
+
used: set[str] = set()
|
|
169
|
+
|
|
170
|
+
def refs(v):
|
|
171
|
+
if isinstance(v, dict):
|
|
172
|
+
t = v.get("texture")
|
|
173
|
+
if isinstance(t, str):
|
|
174
|
+
used.add(t)
|
|
175
|
+
for x in v.values():
|
|
176
|
+
refs(x)
|
|
177
|
+
elif isinstance(v, list):
|
|
178
|
+
for x in v:
|
|
179
|
+
refs(x)
|
|
180
|
+
refs(doc)
|
|
181
|
+
dropped = []
|
|
182
|
+
for f in sorted((root / "textures").glob("*.png")):
|
|
183
|
+
rel = f"textures/{f.name}"
|
|
184
|
+
if rel not in used:
|
|
185
|
+
f.unlink()
|
|
186
|
+
dropped.append(rel)
|
|
187
|
+
return dropped
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def skin_key(name: str) -> str:
|
|
191
|
+
"""The note skin asset of MasterLiveNoteSkin._assetName `name` (LiveAddressablePath.GetLiveNoteSkinAssetPath)."""
|
|
192
|
+
return f"Live/Note/{name}/LiveNoteSkinAsset"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def effect_settings_key(effect: str) -> str:
|
|
196
|
+
"""The note effect settings asset of effect set `effect` (LiveAddressablePath.GetLiveNoteEffectSkinAssetPath)."""
|
|
197
|
+
return f"Effect/Live/NoteEffect/{effect}/LiveNoteEffectAssetSettings"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def effect_keys(cat: Catalog, effect: str) -> list[str]:
|
|
201
|
+
"""The catalog keys of note effect set `effect` and of the lane effects (KEY_PREFIXES), in prefix and catalog
|
|
202
|
+
order."""
|
|
203
|
+
keys: list[str] = []
|
|
204
|
+
for p in KEY_PREFIXES:
|
|
205
|
+
keys += [k for k in cat.keys(p.format(effect=effect)) if k not in keys]
|
|
206
|
+
return keys
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _mono(ex: Exporter, o, cls: str):
|
|
210
|
+
if o is None or o.type.name != "MonoBehaviour" or ex.script_class(o) != cls:
|
|
211
|
+
raise RuntimeError(f"expected a {cls} MonoBehaviour, got {o and o.type.name}")
|
|
212
|
+
return o, o.read_typetree()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def bar_line_prefab(ex: Exporter) -> dict:
|
|
216
|
+
"""The bar line view as a node list: the LiveBarLineView prefab the live scene's bar line container
|
|
217
|
+
instantiates (BAR_LINE_CHAIN from the scene's LiveAllNoteView), its root first."""
|
|
218
|
+
env, graph = ex.load(SCENE_KEY)
|
|
219
|
+
path, cls = NOTE_VIEW
|
|
220
|
+
gos = set(graph.gos_at_path(path))
|
|
221
|
+
comps = [c["component"] for go in gos for c in graph.go[go]["m_Component"]]
|
|
222
|
+
owners = [o for o in env.objects if o.type.name == "GameObject" and o.path_id in gos]
|
|
223
|
+
if len(gos) != 1 or len(owners) != 1:
|
|
224
|
+
raise RuntimeError(f"{SCENE_KEY}: {len(gos)} GameObjects at {path}")
|
|
225
|
+
hits = [c for c in (ex.deref(owners[0], p) for p in comps)
|
|
226
|
+
if c is not None and c.type.name == "MonoBehaviour" and ex.script_class(c) == cls]
|
|
227
|
+
if len(hits) != 1:
|
|
228
|
+
raise RuntimeError(f"{SCENE_KEY}: {len(hits)} {cls} at {path}")
|
|
229
|
+
o, tt = _mono(ex, hits[0], cls)
|
|
230
|
+
for field, want in BAR_LINE_CHAIN:
|
|
231
|
+
o, tt = _mono(ex, ex.deref(o, tt[field]), want)
|
|
232
|
+
root = graph.tf_of_go.get(tt["m_GameObject"]["m_PathID"])
|
|
233
|
+
if root is None or graph.tf[root]["m_Father"]["m_PathID"]:
|
|
234
|
+
raise RuntimeError(f"{SCENE_KEY}: the bar line view is not a prefab root of the scene's bundles")
|
|
235
|
+
return {"key": SCENE_KEY, "reference": ".".join(f for f, _ in BAR_LINE_CHAIN),
|
|
236
|
+
"nodes": ex.hierarchy(env, graph, root)}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def extract(cat: Catalog, player: PlayerData, out_dir: Path, master: Path | None = None,
|
|
240
|
+
options: LiveOptions = LiveOptions()) -> dict:
|
|
241
|
+
"""livenotes/ for the default options, plus the files of the variants `options` offers (the module
|
|
242
|
+
docstring). The variants' objects are exported after the default ones."""
|
|
243
|
+
out_dir = Path(out_dir)
|
|
244
|
+
root = out_dir / "livenotes"
|
|
245
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
ex = Exporter(cat, root, player=player, clip_format="livenotes", follow=("AnimatorController", "SpriteAtlas"))
|
|
247
|
+
doc: dict = {"settings": settings(master, options)}
|
|
248
|
+
skin = doc["settings"]["skin"]
|
|
249
|
+
|
|
250
|
+
# note / line prefabs first: their hierarchies resolve clip binding paths of skin/effect assets
|
|
251
|
+
doc["prefabs"] = {name: ex.export_key(key) for name, key in PREFABS.items()}
|
|
252
|
+
doc["noteSkin"] = ex.export_key(skin_key(skin))
|
|
253
|
+
keys = effect_keys(cat, doc["settings"]["effect"])
|
|
254
|
+
keys += [k for k in SINGLE_KEYS if cat.has(k) and k not in keys]
|
|
255
|
+
assets, failures = {}, {}
|
|
256
|
+
|
|
257
|
+
def export(ks):
|
|
258
|
+
for k in ks:
|
|
259
|
+
try:
|
|
260
|
+
assets[k] = ex.export_key(k)
|
|
261
|
+
except NotImplementedError as e:
|
|
262
|
+
failures[k] = str(e)
|
|
263
|
+
export(keys)
|
|
264
|
+
other_skins = [n for n in doc["settings"].get("skins", {}).values() if n != skin]
|
|
265
|
+
if other_skins:
|
|
266
|
+
doc["noteSkins"] = {n: ex.export_key(skin_key(n)) for n in other_skins}
|
|
267
|
+
names = {int(i): n for i, n in doc["settings"].get("effects", {}).items()}
|
|
268
|
+
for effect, light in options.effect_sets(names):
|
|
269
|
+
if not cat.has(effect_settings_key(effect)):
|
|
270
|
+
if light:
|
|
271
|
+
continue # the game loads the set itself at quality Low (LiveOptions.effect_sets)
|
|
272
|
+
raise KeyError(f"effect set {effect}: {effect_settings_key(effect)} not in the catalog")
|
|
273
|
+
export([k for k in effect_keys(cat, effect) if k not in assets and k not in failures])
|
|
274
|
+
if options.bar_lines:
|
|
275
|
+
doc["prefabs"][BAR_LINE_VIEW] = bar_line_prefab(ex)
|
|
276
|
+
doc["assets"] = assets
|
|
277
|
+
ex.add_path_source(SCENE_KEY, SCENE_SUBTREES)
|
|
278
|
+
unresolved = ex.resolve_pending()
|
|
279
|
+
doc["clips"] = ex.clips
|
|
280
|
+
doc["controllers"] = ex.controllers
|
|
281
|
+
doc["exportFailures"] = failures
|
|
282
|
+
|
|
283
|
+
dropped = _drop_tmp_fonts(doc, root)
|
|
284
|
+
_, shader_summary = ex.dump_shaders(root / "shaders")
|
|
285
|
+
doc["shaders"] = {"index": "shaders/shaders.json", "names": shader_summary["names"]}
|
|
286
|
+
doc["convention"] = ("Unity space (left-handed, Y up); transforms are local TRS; quaternions (x,y,z,w); "
|
|
287
|
+
"colours as serialized (project colour space: see player.json)")
|
|
288
|
+
write_json(root / "notes.json", doc)
|
|
289
|
+
return {"prefabs": len(doc["prefabs"]), "assets": len(assets), "clips": len(ex.clips),
|
|
290
|
+
"controllers": len(ex.controllers), "textures": len(ex.textures) - len(dropped), "droppedFontTextures": dropped,
|
|
291
|
+
"failures": failures, "unresolvedBindings": unresolved, **shader_summary}
|
nnnotes/liveoptions.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Live options that select files: which option variants a live directory carries files for.
|
|
2
|
+
|
|
3
|
+
The game reads its Live options once, when a live boots (LiveDataCreator.CreateBootData, LiveBootDataCreator
|
|
4
|
+
.CreateViewData, LiveSettingCreator.CreateLiveNoteSettings / CreateSESettings); a fresh profile uses option preset 1
|
|
5
|
+
(MasterOptionDefault rows of `_presetId` 1). A live directory holds the files those defaults read. Most options only
|
|
6
|
+
change values the player computes from the data; the options below also change which files a live reads, so a
|
|
7
|
+
viewer can offer them only when the live directory carries those files as well:
|
|
8
|
+
|
|
9
|
+
MirrorChart the score converted mirrored (SsMusicScoreConverter.Load with isMirror):
|
|
10
|
+
score/<chart>.mirror.notes.json, live.json `notesMirror`
|
|
11
|
+
NoteDesignId note skins (MasterLiveNoteSkin): livenotes settings.skins, noteSkins
|
|
12
|
+
NoteEffectId note effect sets (MasterLiveNoteEffectSkin): livenotes settings.effects, their assets (the lane
|
|
13
|
+
effect set is always effect001, LaneEffectLoadStep.GetAssetPath)
|
|
14
|
+
LiveQuality qualities (MasterLiveQualitySettings); Low (2) loads a note effect set's `<name>Light` variant
|
|
15
|
+
where it exists (NoteSkinLoadStep.GetLightQualityAddress); the chart manifest's
|
|
16
|
+
`options.LiveQuality`
|
|
17
|
+
NoteSePatternId note sound sets (MasterLiveNoteSe groups): audio/live-audio.json noteSe.groups and their sounds
|
|
18
|
+
MeasureLineDisplay the bar lines (LiveAllBarLineView): the bar line view the live scene's LiveBarLineViewContainer
|
|
19
|
+
instantiates (its _elementPrefab), livenotes prefabs.bar_line_view, and its sprite's texture
|
|
20
|
+
|
|
21
|
+
and the option tables: the preset-1 default and the range of every option item the player offers
|
|
22
|
+
(livenotes settings.optionDefaults / optionRanges), implied by any of the above.
|
|
23
|
+
|
|
24
|
+
A request names options as the game does (the names the player's settings use), from command-line specs:
|
|
25
|
+
`MirrorChart`, `NoteDesignId` (every value the master data has) or `NoteDesignId=1,3` (those values), ...,
|
|
26
|
+
`defaults` (the option tables only) and `all` (every option with every value). `resolve` checks a request against
|
|
27
|
+
the master data and gives a LiveOptions; each offered option lists its values including the preset-1 default. An
|
|
28
|
+
empty LiveOptions is the default output, byte for byte.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import itertools
|
|
33
|
+
from dataclasses import dataclass, replace
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from .score import master_table
|
|
37
|
+
|
|
38
|
+
# App.Options.OptionItemType (every value but None 0), the names of MasterOptionDefault / MasterOptionRange
|
|
39
|
+
# `_optionItemType`
|
|
40
|
+
OPTION_ITEM_TYPES = {
|
|
41
|
+
1: "NoteSpeed", 2: "NoteTiming", 3: "ChartPosition", 4: "MirrorChart", 5: "AssistMode", 6: "LiveQuality",
|
|
42
|
+
7: "Vibration",
|
|
43
|
+
100: "FastSlowDisplay", 101: "PerfectFastSlowDisplay", 102: "JudgeOffsetMsDisplay", 103: "JudgeResultPositionType",
|
|
44
|
+
104: "JudgePosition", 105: "JudgePositionDisplay", 106: "SlideOpacity", 107: "GuideOpacity",
|
|
45
|
+
108: "SimultaneousLineDisplay", 109: "MeasureLineDisplay", 110: "MvQuality", 111: "MvDataRetention",
|
|
46
|
+
112: "LiveSkillEffect", 113: "ComboEffect", 114: "DamageEffect", 115: "StageEffect", 116: "FcAcChallengeAssist",
|
|
47
|
+
117: "PreLiveSimpleOption", 118: "FrameRate", 119: "GekisouEffect",
|
|
48
|
+
200: "ScreenMode", 201: "BackgroundBrightness", 202: "MvModeBrightness", 203: "BackgroundSwitch",
|
|
49
|
+
205: "SkillEffectDisplay", 206: "ComboCountDisplay", 207: "JudgeDetailDisplay", 208: "ContinuationEffectDisplay",
|
|
50
|
+
300: "LaneOpacity", 301: "GuidelineOpacity", 302: "GuidelineCount", 303: "GekisouDisplay",
|
|
51
|
+
304: "GekisouDisplayPositionChange", 305: "LiveSkinId", 306: "NoteDesignId", 307: "NoteEffectId",
|
|
52
|
+
308: "NoteStartPosition", 309: "LiveSkillActivationPositionDisplay", 310: "GekisouSimpleEffect",
|
|
53
|
+
400: "SystemBgmVolume", 401: "SystemSeVolume", 402: "SystemVoiceVolume", 403: "SystemBgmMute",
|
|
54
|
+
404: "SystemSeMute", 405: "SystemVoiceMute",
|
|
55
|
+
410: "LiveMusicVolume", 411: "LiveNoteSeVolume", 412: "LiveSeVolume", 413: "LiveVoiceVolume",
|
|
56
|
+
414: "LiveMusicMute", 415: "LiveNoteSeMute", 416: "LiveSeMute", 417: "LiveVoiceMute",
|
|
57
|
+
420: "NoteSePatternId", 421: "UseIndividualNoteSe",
|
|
58
|
+
430: "EmptyTapSeId", 431: "EmptyTapSeVolume", 432: "TapSeId", 433: "TapSeVolume", 434: "FlickSeId",
|
|
59
|
+
435: "FlickSeVolume", 436: "SideFlickSeId", 437: "SideFlickSeVolume", 438: "SlideSeId", 439: "SlideSeVolume",
|
|
60
|
+
440: "TraceSeId", 441: "TraceSeVolume",
|
|
61
|
+
450: "GekisouTapSeId", 451: "GekisouTapSeVolume", 452: "GekisouFlickSeId", 453: "GekisouFlickSeVolume",
|
|
62
|
+
454: "GekisouSideFlickSeId", 455: "GekisouSideFlickSeVolume", 456: "GekisouSlideSeId",
|
|
63
|
+
457: "GekisouSlideSeVolume", 458: "GekisouTraceSeId", 459: "GekisouTraceSeVolume",
|
|
64
|
+
460: "EmptyTapSeMute", 461: "TapSeMute", 462: "FlickSeMute", 463: "SideFlickSeMute", 464: "SlideSeMute",
|
|
65
|
+
465: "TraceSeMute", 466: "GekisouTapSeMute", 467: "GekisouFlickSeMute", 468: "GekisouSideFlickSeMute",
|
|
66
|
+
469: "GekisouSlideSeMute", 470: "GekisouTraceSeMute",
|
|
67
|
+
500: "BluetoothNoteTiming", 501: "BluetoothNoteChartPosition",
|
|
68
|
+
510: "BluetoothSystemBgmVolume", 511: "BluetoothSystemSeVolume", 512: "BluetoothSystemVoiceVolume",
|
|
69
|
+
513: "BluetoothSystemBgmMute", 514: "BluetoothSystemSeMute", 515: "BluetoothSystemVoiceMute",
|
|
70
|
+
520: "BluetoothLiveMusicVolume", 521: "BluetoothLiveNoteSeVolume", 522: "BluetoothLiveSeVolume",
|
|
71
|
+
523: "BluetoothLiveVoiceVolume", 524: "BluetoothLiveMusicMute", 525: "BluetoothLiveNoteSeMute",
|
|
72
|
+
526: "BluetoothLiveSeMute", 527: "BluetoothLiveVoiceMute",
|
|
73
|
+
600: "QualitySetting", 601: "PurchaseAlert", 602: "LiveBoostFullRecoveryNotification",
|
|
74
|
+
603: "LateNightNotification", 604: "OfflineBonusReachedMaxRewardsNotification",
|
|
75
|
+
999: "SongTitleDisplay",
|
|
76
|
+
}
|
|
77
|
+
# The items the player offers beyond those the note runtime reads (livenotes.OPTION_ITEMS): the option tables add
|
|
78
|
+
# their defaults and ranges. NoteTiming; AssistMode, Vibration, FcAcChallengeAssist, FrameRate (accepted without an
|
|
79
|
+
# effect or at the default); the live volumes and mutes; the note sound set, the individual switch and the per-type
|
|
80
|
+
# sound ids, volumes and mutes (empty tap, tap, flick, side flick, slide, trace).
|
|
81
|
+
PLAYER_ITEMS = (2, 5, 7, 116, 118, *range(410, 418), 420, 421, *range(430, 442), *range(460, 466))
|
|
82
|
+
|
|
83
|
+
# option name -> (OptionItemType, master table and column of its values); MirrorChart is a switch
|
|
84
|
+
ENUM_OPTIONS = {
|
|
85
|
+
"NoteDesignId": (306, "MasterLiveNoteSkin", "_id"),
|
|
86
|
+
"NoteEffectId": (307, "MasterLiveNoteEffectSkin", "_id"),
|
|
87
|
+
"LiveQuality": (6, "MasterLiveQualitySettings", "_quality"),
|
|
88
|
+
"NoteSePatternId": (420, "MasterLiveNoteSe", "_groupID"),
|
|
89
|
+
}
|
|
90
|
+
SWITCH_OPTIONS = {"MirrorChart": 4, "MeasureLineDisplay": 109}
|
|
91
|
+
OPTION_NAMES = (*SWITCH_OPTIONS, *ENUM_OPTIONS)
|
|
92
|
+
TABLES = "defaults" # request item: the option tables only
|
|
93
|
+
ALL = "all" # request item: every option with every value (and the tables)
|
|
94
|
+
FIELDS = {"MirrorChart": "mirror", "NoteDesignId": "designs", "NoteEffectId": "effects", "LiveQuality": "qualities",
|
|
95
|
+
"NoteSePatternId": "se_patterns", "MeasureLineDisplay": "bar_lines"}
|
|
96
|
+
# options whose files are pictures (read-set variants combine them); the note sounds do not depend on them
|
|
97
|
+
VISUAL_OPTIONS = ("MirrorChart", "NoteDesignId", "NoteEffectId", "LiveQuality")
|
|
98
|
+
LOW_QUALITY = 2 # LiveQuality Low: the `<name>Light` effect sets (NoteSkinLoadStep.GetLightQualityAddress)
|
|
99
|
+
LIGHT_SUFFIX = "Light"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class OptionSpecError(ValueError):
|
|
103
|
+
"""A malformed --live-option spec, or a value the master data does not have."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_specs(specs) -> dict:
|
|
107
|
+
"""Command-line specs -> a request (JSON-able): {option name: "all" | [values] | True, "defaults": True}.
|
|
108
|
+
`Name` offers every value of an option (a switch: both), `Name=v,v` those values, `defaults` the option tables,
|
|
109
|
+
`all` every option. Repeated names merge."""
|
|
110
|
+
req: dict = {}
|
|
111
|
+
for spec in specs or ():
|
|
112
|
+
s = spec.strip()
|
|
113
|
+
name, eq, vals = s.partition("=")
|
|
114
|
+
name = name.strip()
|
|
115
|
+
if name in (TABLES, ALL):
|
|
116
|
+
if eq:
|
|
117
|
+
raise OptionSpecError(f"--live-option {name} takes no values")
|
|
118
|
+
req[name] = True
|
|
119
|
+
continue
|
|
120
|
+
if name in SWITCH_OPTIONS:
|
|
121
|
+
if eq:
|
|
122
|
+
raise OptionSpecError(f"--live-option {name} is a switch: give it without values")
|
|
123
|
+
req[name] = True
|
|
124
|
+
continue
|
|
125
|
+
if name not in ENUM_OPTIONS:
|
|
126
|
+
raise OptionSpecError(f"--live-option {name!r}: one of {', '.join((*OPTION_NAMES, TABLES, ALL))}")
|
|
127
|
+
if not eq:
|
|
128
|
+
req[name] = "all"
|
|
129
|
+
continue
|
|
130
|
+
try:
|
|
131
|
+
values = sorted({int(v) for v in vals.split(",") if v.strip()})
|
|
132
|
+
except ValueError:
|
|
133
|
+
raise OptionSpecError(f"--live-option {s}: values are integers, comma-separated") from None
|
|
134
|
+
if not values:
|
|
135
|
+
raise OptionSpecError(f"--live-option {s}: no values")
|
|
136
|
+
old = req.get(name)
|
|
137
|
+
if old != "all":
|
|
138
|
+
req[name] = sorted(set(values) | set(old or ()))
|
|
139
|
+
return req
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class LiveOptions:
|
|
144
|
+
"""The file-selecting options a live directory carries files for. Each enum option: the offered values in
|
|
145
|
+
ascending order, including the preset-1 default; () when only the default is offered. Each switch: offered
|
|
146
|
+
(both values) or not. `tables`: the option tables (the default and range of every item the player offers)."""
|
|
147
|
+
mirror: bool = False
|
|
148
|
+
bar_lines: bool = False
|
|
149
|
+
designs: tuple = ()
|
|
150
|
+
effects: tuple = ()
|
|
151
|
+
qualities: tuple = ()
|
|
152
|
+
se_patterns: tuple = ()
|
|
153
|
+
tables: bool = False
|
|
154
|
+
defaults: tuple = () # ((option name, preset-1 value), ...) of the enum options, from the master data
|
|
155
|
+
|
|
156
|
+
def __bool__(self) -> bool:
|
|
157
|
+
return bool(self.tables or self.mirror or self.bar_lines or self.designs or self.effects or self.qualities
|
|
158
|
+
or self.se_patterns)
|
|
159
|
+
|
|
160
|
+
def default(self, name: str) -> int:
|
|
161
|
+
return dict(self.defaults)[name]
|
|
162
|
+
|
|
163
|
+
def values(self, name: str) -> list:
|
|
164
|
+
"""The values of option `name` the files support (the default alone when it is not offered)."""
|
|
165
|
+
if name in SWITCH_OPTIONS:
|
|
166
|
+
return [False, True] if getattr(self, FIELDS[name]) else [False]
|
|
167
|
+
return list(getattr(self, FIELDS[name]) or (self.default(name),))
|
|
168
|
+
|
|
169
|
+
def manifest_options(self) -> dict | None:
|
|
170
|
+
"""The chart manifest's `options`: the values of the options the player takes from the manifest
|
|
171
|
+
(LiveQuality; the other options are offered by the data itself), None when only the default is offered."""
|
|
172
|
+
return {"LiveQuality": list(self.qualities)} if self.qualities else None
|
|
173
|
+
|
|
174
|
+
def effect_sets(self, names: dict) -> list[tuple[str, bool]]:
|
|
175
|
+
"""The note effect sets (MasterLiveNoteEffectSkin._assetName, `names`: id -> name) the files may carry beyond
|
|
176
|
+
the default set, as (name, is a Light variant): the other offered sets, and with LiveQuality Low offered the
|
|
177
|
+
`Light` variant of every offered set (the default one included), in id order. The game loads a Light
|
|
178
|
+
variant only where it exists and the set itself otherwise (NoteSkinLoadStep.CollectDownloadAddresses:
|
|
179
|
+
ResourceManager.ExistsAsset; LoadNoteEffectSkinAssetSettingsAsync falls back when the Light load fails)."""
|
|
180
|
+
if not self.defaults:
|
|
181
|
+
return []
|
|
182
|
+
low = LOW_QUALITY in self.qualities
|
|
183
|
+
out = []
|
|
184
|
+
for i in self.values("NoteEffectId"):
|
|
185
|
+
if i != self.default("NoteEffectId"):
|
|
186
|
+
out.append((names[i], False))
|
|
187
|
+
if low:
|
|
188
|
+
out.append((names[i] + LIGHT_SUFFIX, True))
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
def read_variants(self) -> list[dict]:
|
|
192
|
+
"""The settings (option name -> value, non-default values only) whose read sets, with the default one,
|
|
193
|
+
make the union of the files a chart reads over every offered variant: every combination of the offered
|
|
194
|
+
picture options (the note skin, the effect set and its quality variant, and the mirrored score's flick
|
|
195
|
+
directions select pictures together), then the bar lines alone (their sprite does not depend on the other
|
|
196
|
+
options) and each other note sound set alone (sounds do not depend on the pictures). The all-default
|
|
197
|
+
combination is left out."""
|
|
198
|
+
if not self.defaults:
|
|
199
|
+
return []
|
|
200
|
+
dims = []
|
|
201
|
+
for name in VISUAL_OPTIONS:
|
|
202
|
+
vals = self.values(name)
|
|
203
|
+
default = False if name in SWITCH_OPTIONS else self.default(name)
|
|
204
|
+
dims.append([(name, v) for v in [default, *[v for v in vals if v != default]]])
|
|
205
|
+
out = []
|
|
206
|
+
for combo in itertools.product(*dims):
|
|
207
|
+
s = {n: v for n, v in combo if v != (False if n in SWITCH_OPTIONS else self.default(n))}
|
|
208
|
+
if s:
|
|
209
|
+
out.append(s)
|
|
210
|
+
if self.bar_lines:
|
|
211
|
+
out.append({"MeasureLineDisplay": True})
|
|
212
|
+
out += [{"NoteSePatternId": g} for g in self.se_patterns if g != self.default("NoteSePatternId")]
|
|
213
|
+
return out
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def preset_defaults(master: Path) -> dict[int, str]:
|
|
217
|
+
"""OptionItemType -> MasterOptionDefault preset-1 value string (a fresh profile's options)."""
|
|
218
|
+
return {r["_optionItemType"]: r["_valueString"] for r in master_table(master, "MasterOptionDefault")
|
|
219
|
+
if r["_presetId"] == 1}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def resolve(request: dict | None, master: Path | None) -> LiveOptions:
|
|
223
|
+
"""The LiveOptions of a request (parse_specs) with the master data: `all` values from its tables, requested
|
|
224
|
+
values checked against them, the preset-1 defaults added. An empty request (or None) is LiveOptions()."""
|
|
225
|
+
if not request:
|
|
226
|
+
return LiveOptions()
|
|
227
|
+
if master is None:
|
|
228
|
+
raise OptionSpecError("--live-option needs the master data")
|
|
229
|
+
master = Path(master)
|
|
230
|
+
everything = bool(request.get(ALL))
|
|
231
|
+
preset = preset_defaults(master)
|
|
232
|
+
defaults, fields = [], {}
|
|
233
|
+
for name, (item, table, column) in ENUM_OPTIONS.items():
|
|
234
|
+
have = sorted({int(r[column]) for r in master_table(master, table)})
|
|
235
|
+
d = int(preset[item])
|
|
236
|
+
defaults.append((name, d))
|
|
237
|
+
want = "all" if everything else request.get(name)
|
|
238
|
+
if want is None:
|
|
239
|
+
continue
|
|
240
|
+
want = have if want == "all" else list(want)
|
|
241
|
+
bad = [v for v in want if v not in have]
|
|
242
|
+
if bad:
|
|
243
|
+
raise OptionSpecError(f"--live-option {name}: {', '.join(map(str, bad))} not in {table} "
|
|
244
|
+
f"({', '.join(map(str, have))})")
|
|
245
|
+
vals = tuple(sorted(set(want) | {d}))
|
|
246
|
+
if len(vals) > 1:
|
|
247
|
+
fields[FIELDS[name]] = vals
|
|
248
|
+
switches = {FIELDS[name]: everything or bool(request.get(name)) for name in SWITCH_OPTIONS}
|
|
249
|
+
out = LiveOptions(defaults=tuple(defaults), **switches, **fields)
|
|
250
|
+
return replace(out, tables=True) if (out or request.get(TABLES)) else out
|