Phigros 4.0.1 第九章客户端全解:从启动到退出,这一章在程序里究竟发生了什么

10437 字
52 分钟
Phigros 4.0.1 第九章客户端全解:从启动到退出,这一章在程序里究竟发生了什么
Phigros 4.0.1 第九章客户端全解:从启动到退出,这一章在程序里究竟发生了什么
速览(想直接看结论就看这五条)
  1. 入场:第八章《Distorted Fate》880,000 分 → MainStory9;12 首曲各有一把 C9*Unlocked 钥匙。
  2. 两台状态机:进度 Step(5 步,存档键 Chapter9Phase2Step)与演出 RuntimeStage(8 段),推进发生在结算界面。
  3. 密码不是比对,而是 SHA-512 切片派生 AES-256-CBC 的 Key/IV,解得开 c9s.* 即正确(8 个秘密包,含 S6 终章与 458 音符秘密谱)。
  4. 退出应用是流程的一步:QuitForMessageResume() 会先落盘再 Application.Quit(),冷启动由 Phigros.JumpToMessage 把你送回 Message。
  5. 噪域 / LIFE / 课题乱码都是真实机制:噪域=按音乐时间轴推的 noise 通道;LIFE 档位 10/50/100/200;课题界面的乱码是 8–12 位随机占位串。

这篇是程序向的完整拆解:不是零散知识点,而是「第九章这一章在客户端里,从你按下启动到它把你请回标题界面,程序依次做了什么」。调用关系由 bl 指令反查(c9_callgraph.py,§12),RVA 来自 dump.cs,都可复核。剧情向整理在剧情篇。

出处与工具:Phigros_4.0.1_TapTap.apk.1(Unity 2022.3.62f2 / IL2CPP / Addressables);Il2CppDumper 6.7.46(.NET 8 用 DOTNET_ROLL_FORWARD=Major);libil2cpp.so + global-metadata.dat + data.unity3d + catalog.json + *.bundle。

官方第九章章节封面
官方第九章章节封面


1. 数据地图#

内容位置
章节表 / 328 首歌(曲名·曲师·定数·谱师·解锁键)data.unity3d → level0 场景的 GameInformation 组件(16 万字节,无 TypeTree,手写解析)
类·字段·方法·RVAdump.cs(74,912 个带 RVA 的方法;第九章 242 个类块)
六语言文本(档案名、剧情页)data.unity3d 内 MonoBehaviour 的 MultiLanguageTextString(简中/繁中/EN/JP/KR 内联)
归档与收藏目录level22、sharedassets22.assets pid=143(SaturnOS 收藏库)
谱面Assets/Tracks/{songsId}/Chart_{EZ,HD,IN,AT,SP}.json(dependency_key 才指向真实 bundle)
加密秘密资产8 个 c9s.*(C9SecretAssetBundleProvider,§8)

来源与核对(外部链接)

章节名与更新节奏:官方公告(TapTap 4.0 更新公告)+ Phigros Wiki(JP) 更新履历(4.0.0 = 09-25 追加第九章;4.0.1 = 10-02 追加后篇)→ 与客户端本地化条目 穹顶孤舟 / Solivault / 穹窿の孤舟 一致。 LIFE 档位:客户端读出 10/50/100/200;社区攻略(全部歌曲解锁流程、解密流程)描述一致,但「Bad/Miss 扣 2」只有社区来源。 12 首曲:官方公告只放 3 首 + 3 段谜题串(Atbash 类),其余靠解密;客户端 Phigros2.Chapter9.Key 给出全部 12 个 songsId,与社区汇总一致。

2. 全流程:从启动到退出#

第九章全流程泳道图:场景 / 状态 / 存档键,以及全库 5 个退出点
第九章全流程泳道图:场景 / 状态 / 存档键,以及全库 5 个退出点

主线是一条单向状态机(进度 5 步 + 演出 8 段)。每次跨步都发生在「结算 / 特定场景启动 / 玩家输入」这三个时机。下面按玩家实际经历的顺序走,并标出谁触发、切了哪个场景、写了什么存档。

2.1 启动:先把解锁与密钥恢复回来#

GameInformation.Awake() [RVA 0x1CB7C6C]
└─ C9SecretAssetBundleProvider.SetPassword(...) // ★ 开机即恢复密码
GameProgressSaveModule.LoadFromLocal → <LoadFromLocal>g__LoadChapter9Keys|38_7 [0x1D65C08]
└─ Chapter9Phase2Progress.LoadCurrentStep() // 读回 Chapter9Phase2Step
GameProgressSaveModule.Apply → <Apply>g__SaveChapter9Keys|36_7 [0x1D64168]
└─ SetPassword(...) + 写回 C9*Unlocked // 存档应用时重设密钥

要点:密码与解锁状态进存档(GameProgressSaveModule 另有 LoadV1 / LoadV5,经 BinaryUtils.GetString 解二进制存档)。所以你输过一次的密码重启后依然生效,秘密包不会再问你第二遍。

2.2 进章:Phase 1(选曲)#

LaunchControl.ToChapter9() // DontDestroyOnLoad 保活控制器
├─ EnterPhase1Coroutine() / EnterPhase2Coroutine() // 按存档决定进哪一形态
└─ 淡出 BGM(AddressableAudioSource.Volume/CancelFadeIn)→ 进 Chapter9SelectMusic
Chapter9Phase1Control.Start() // 选曲页总控
├─ 地图:Chapter9MapElement.Sync() / Chapter9MapElementLink.<FillRoutine> → AddMapFocus/RemoveMapFocus
├─ 解锁与收藏:Chapter9CollectionUnlockSprite.UpdateDisplay → UnlockAnimation → ApplyUnlockedAppearance
├─ 曲目详情:Chapter9SongDetailsControl.Open/UpdateInfo(不同难度 LoadDifferentCoverImage)
└─ 开谱:Chapter9SongLevelStartControl.PrepareBackground → <Start…> → SceneManager.LoadScene
  • 选曲元素三态:Locked / Unlocked / Read;地图聚焦由 IChapter9MapFocusProvider + AddMapFocus/RemoveMapFocus 驱动。
  • 离开时成对释放:Chapter9Phase1Control.OnDestroy → ReleasePhase1EndingAmbient();Back() → Chapter9SongLevelStartControl.DestroyBackground()(RenderTexture.Release() + Destroy)。

2.3 难度动画:选难度是「六路视频 + 定格 + 畸变」#

DesultorySignalsIntroControl.Start() → MainVideoPlayer() // 开场 CG
→ PlayDiffVideo(string diff) → DiffVideoPlayer() // 难度选择画面
→ 所选难度:ez / hd / ins / at 对应 VideoPlayer
→ NextScene() → RawImage.set_texture → SceneManager.LoadScene
  • 字段:introCG / diffSelect / ez / hd / ins / at(6 个 VideoPlayer)、videoRT(RenderTexture)、lensDistortion、freezeFrame、selectButtons[]、PlayStartWindow = 0.5、waitTimeOut / introVideoTime / diffSelectTime / diffVideoTime。
  • 转场:CutInAnimator(切入)、TriFrameCutOutAnimator(三角框切出)、BlackScreenTransition(BeginTransition 里 DontDestroyOnLoad,<TransitionRoutine> → LoadSceneAsync)。

六段视频(字段与触发时机一一对应;点开播放,全部资产见素材馆):

段字段何时播
开场 CGintroCG进入该曲的演出开场
难度选择画面diffSelect「开始」后、选难度那一屏(点 selectButtons)
EZ / HD / IN / ATez / hd / ins / at选中对应难度时各自的切入短片(定数 10 / 14 / 17 / 18)
开场 CG(introCG)
开场 CG(introCG)
难度选择画面(diffSelect)
难度选择画面(diffSelect)
EASY 切入(ez · 定数 10)
EASY 切入(ez · 定数 10)
AT 切入(at · 定数 18)
AT 切入(at · 定数 18)
▶ 六段视频(EZ / HD / IN / AT / diffSelect / introCG)
  • 转场:CutInAnimator(切入)、TriFrameCutOutAnimator(三角框切出)、BlackScreenTransition(BeginTransition 里 DontDestroyOnLoad,<TransitionRoutine> → LoadSceneAsync)。

六段视频(原样放出)——字段与触发时机一一对应,全部资产见素材馆:

段字段何时播
开场 CGintroCG进入该曲的演出开场
难度选择画面diffSelect「开始」后、选难度那一屏(点 selectButtons)
EZ / HD / IN / ATez / hd / ins / at选中对应难度时各自的切入短片

Desultory Signals 的六路视频(字段与画面卡片对照)
Desultory Signals 的六路视频(字段与画面卡片对照)

- 隐藏曲过场:`ChallengeModeControl.hiddenSongVideoPlayer` + `EMOverlayTime`/`ETCOverlayTime`(两首隐藏曲的 PV 覆盖层)。

2.4 打关:进度不在关卡里推进,而在结算里推进#

LevelOverControl.<>c__DisplayClass43_0.<Start>
├─ g__HandleTrueHomeTrueWorldEvent|15() → Chapter9Phase2Progress.TryAwaitAmetrine()
├─ g__HandelAmetrineEvent|16() → …TryCompleteAmetrine(bool judgeCheckFailed)
├─ g__HandlePetrichorEvent|17() → …TryCompletePetrichor()
└─ PlayerPrefs.Save()
  • TryAdvance(expected, next) 内部:LoadCurrentStep() 比对 → SaveStep(expected) 落盘(Chapter9Phase2Step)。没调用 TryAdvance 就等于没推进。
  • 语义参数:Ametrine 的判定参数叫 judgeCheckFailed(要「挑战失败」参与),Petrichor 要「挑战通过」(IsPetrichorChallengePassed)。
  • 挑战模式进出:ChallengeModeControl.<Start>d__21 / <EnterLevel> → LoadScene;ChallengeModeOverControl.<TurnToNextScene> → LoadScene。

2.5 Phase 2:换形态,并进入谜题 / 推演#

Chapter9Phase1Control.ToPhase2() → Progress.TryBeginSecondPhase() + PlayerPrefs.Save()
→ SceneManager.LoadSceneAsync(...) // ★ 异步切形态
Chapter9Phase2Control.Start() → Progress.TryBeginSecondPhase()(兜底)+ ApplyMapCover()
Chapter9Phase2Control.<EnterPuzzle…> / <TurnToNextScene…> → SceneManager.LoadScene
  • 形态标记 Chapter9LastSelectMusicIsPhase2;两套封面 #ChapterCover/MainStory9.jpg 与 MainStory9_2.jpg。
  • 首次进入走 FirstEnterPhase2Control:挂 VideoPlayer 的 prepareCompleted / loopPointReached,OnFinished() → TryAdvance(...) + PlayerPrefs.Save();超时走 WatchVideoTimeout(日志 [ToPhase2] Video timed out. Entering Chapter9Phase2.)。
  • 启动器(OS 风格):SaturnOSLaunchControl.<ToChapter8Coroutine> / <ToCollectionCoroutine> → LoadSceneAsync;<OpenLoadCoroutine> 里另有 Application.Quit(...) 分支。

2.6 谜题与推演#

Chapter9PuzzlePiece.Awake() → Image.alphaHitTestMinimumThreshold = 0.2
Chapter9PuzzleControl.OnPieceCompleted() → 完成音效 + TweenCompletePostEffects
→ PlayShowPopupExposure() → Chapter9PuzzlePopupControl.TextReplacement
→ ConfirmNextStage() → <PlayNextStage…> → Chapter9Phase2Session.TryBeginBaselineChallenge(config)
→ SceneManager.LoadScene
C9DeductionControl.Start()/Update() → 打字机(UpdateTypingText / RenderPage)
C9DeductionControl.<FinishAndWhiteOut…> → SceneManager.LoadScene // 推演收尾 → 白屏 → 下一场景
C9AfterDeductionControl.<Start>d… → PlayerPrefs.Save + VideoPlayer.Prepare/Play → <StartNextScene> → LoadScene
  • C9DeductionControl:EnsureSceneReferences、PrepareVisualLayers(CreateVignetteTexture / Sprite.Create)、RenderPage(StopCoroutine + Destroy 旧行);OnDestroy → StopAllCoroutines + DestroyRuntimeVisuals。
  • 白屏:Chapter9WhiteoutTransition.—TryCaptureScreen(Texture2D.ReadPixels+Graphics.Blit)→ BlitToMask→AttachToCamera(PostProcessingManager.AddEffects)→ <PlayOut…>→LoadScene;Teardown / ReleaseTexture / EndGlitchAudio成对释放,故障音DontDestroyOnLoad` 跨场景保留。

2.7 密码:一次输入,换来整包秘密资产#

Chapter9InputPasswordControl.Submit() [0x1D486FC]
├─ C9SecretAssetBundleProvider.SetPassword(string) [0x1D26B70]
│ SHA512 + UTF8 → ComputeHash → GetSubArray[0:32] / [32:48] → Aes.Key / Aes.IV
│ → Addressables.LoadAssetAsync<object>(...) // 触发秘密资产重载
└─ <Submit>b__3 → FadeToBlackThenStartC9S6() [0x1D48B34] → SceneManager.LoadScene(string)
  • 失败演出:PlayWrongPasswordOptics()(OpticsStartFov = 110)→ RunWrongPasswordOptics();PlayChangeColor() → RunChangeColor()(配 Chapter9ColorOverlayControl.ToRed/ToBlue);音效 correctPasswordClip / wrongPasswordClip(PlayPasswordSeClip 里 DontDestroyOnLoad)。
  • 输入界面 Start() 只做一件事:关掉 passwordSe.playOnAwake;OnDestroy() 收协程 + Destroy。

2.8 收束:S6 → Hate → DS → Message → HappyEnding → Credits#

C9S6Unlock.EnterLevel() → RawImage.set_texture → SceneManager.LoadScene // 进 S6
DesultorySignalsIntroControl.NextScene() → LoadScene // DS 开场后进关
HateGlitchEffect.Start() → Chapter9Phase2Progress.LoadCurrentStep() // 故障强度=当前步
Chapter9Phase2Session.TryResumeMessage(levelIndex) → LoadCurrentStep() // Message 续玩
C9StoryControl.<TurnToNextScene…> → LoadScene // 秘密剧情收尾
  • 演出层:HateLevelEffects / MessageLevelEffects / DesultorySignalsLevelEffects / C9S6LevelEffect 都是 Start() → PostProcessingManager.AddEffects(),Update() → AnimationDirector.Evaluate(nowTime);MessageLevelEffects.Update 还会 Destroy 临时对象。
  • 收尾断言:Tried to complete Chapter 9 outside the valid credits state.、Song {0} cannot complete while the session stage is {1}.;Credits 之后可从「关于」页补完(CompleteChapterFromAboutUs → LoadCurrentStep)。

2.9 退出:程序在哪里主动退出(共 5 处)#

「退出应用」本身是流程的一步,而且不止一处。全库 Application.Quit 的静态调用点(bl 反查)一共 5 个:

#调用者退出条件证据
1BaselineCourseLevelMod.QuitForMessageResume()基准课判定后走「退出再续 Message」分支:先 SaveManagement.SaveBool/SaveInt + PlayerPrefs.Save(),再 Application.Quit(exitCode≠0)ResolveNextScene() RVA 0x1D1E43C(bl 0x1D1E540)→ QuitForMessageResume() RVA 0x1D1E540(bl 0x1CC311C/0x1CC31D0/0x3C53F20/0x3C168C8)
2SaturnOSLaunchControl.<OpenLoadCoroutine>d__23.MoveNext()屏幕宽高比 ≥ 3 就退出(Screen.width/height,cmp w8,#3,mov w0,#-103)状态机 RVA 0x1D3A404,sdiv+cmp #3 @0x1D3A4C0-C8,bl Application.Quit @0x1D3A4F0
3SplashScene.<GameStart>d__21.MoveNext()隐私政策 / 防沉迷检查返回 false → Application.Quit(0)RVA 0x1DB1168;CheckPrivacyPolicy @0x1DB12C4、AntiAddiction.CheckAntiAddiction @0x1DB1398、tbnz @0x1DB1470、Quit @0x1DB1494
4SimpleCameraController.Update()按 Esc(KeyCode.Escape = 27)直接退出——Unity 官方示例脚本残留且仍在构里生效RVA 0x32AA9F0;mov w0,#0x1b @0x32AAA44、bl Application.Quit() @0x32AAA70
5(非游戏)LCIMConversation.Quit()IM 会话退出,与游戏无关,列出以防误判RVA 0x3505AD4

第 1 处的完整机制(这才是「退出」在第九章里的正式用途):

BaselineCourseLevelMod.OnEnable() → ProgressControl.LevelBreak(nextScene, ResolveNextScene 委托) // 0x1D86620
BaselineCourseLevelMod.ResolveNextScene() [0x1D1E43C]
├─ IsExpectedLevel() // 是不是该在的关卡
├─ Chapter9Phase2Session.TryCompleteCurrentSong()
├─ Chapter9Phase2Session.Reset()
└─ QuitForMessageResume() [0x1D1E540]
SaveManagement.SaveBool(key, bool) + SaveManagement.SaveInt(key, int) + PlayerPrefs.Save()
→ Application.Quit(exitCode)

也就是:「下一场景」由一个委托动态解析(Func<string> resolveScene),它的一个返回值分支就是「退出游戏」——用「落盘 + 退出 + 冷启动重放」来代替一次场景切换。

冷启动怎么把你接回 Message
  • 落盘:C9PendingMessageAfterQuit / C9PendingMessageLevel(是真正的 ldstr 字面量)
  • 读回:BootSceneControl.TryEnterPendingMessage() 0x1CCDAB8 → SceneManager.LoadScene 0x1CCDC3C
  • 跳转基建:JumpToTargetSession:LevelScenePath = "Assets/Phigros2/Level/Level.unity"、GameInformationPrefabPath = "…/BootScene/GameInformation.prefab"、DifficultyIndexIn = 2;DSInfo / HateInfo / MessageInfo 各带 PrefKey = "Phigros.JumpToDS / JumpToHate / JumpToMessage"、LevelModAddress、SongsId;配套 *SessionBootStrap(只有 IsActive)
  • 结论:「退出 → 重进 → 直接落在指定歌曲的课题套件里」是设计好的机制,不是崩溃。

离开时的清理(与保活成对):

界面 / 组件退出动作
Chapter9Phase1Control.OnDestroy / Back()ReleasePhase1EndingAmbient() / DestroyBackground()
Chapter9SongLevelStartControl.DestroyBackgroundRenderTexture.Release() + Destroy
Chapter9WhiteoutTransition.TeardownReleaseTexture / EndGlitchAudio
C9StoryControl / C9DeductionControl / C9StoryPostEffects.OnDestroyStopAllCoroutines + DestroyRuntimeVisuals / DestroyRuntimeEffect
Chapter9InputPasswordControl.OnDestroyStopCoroutine + Destroy
GameInformation.DestroyMainForRestart()Object.Destroy(重启用)
LaunchControl / WhiteoutTransition / BlackScreenTransition反向:DontDestroyOnLoad 保活

2.10 场景名:确证的四个 + 共用槽位#

SceneManager.LoadScene 的调用者共 48 处(第九章相关 20 处)。把每条调用点回溯 adrp+ldr 取字面量槽位后可见:全部应用侧切场景只用到 21 个不同槽位,同槽位=同场景名。可确证的:

场景名出处触发者
Chapter9SelectMusicChapter9UnlockNavigation.SelectMusicSceneLaunchControl.EnterPhase1Coroutine(LoadSceneAsync)
Chapter9Phase2Chapter9UnlockNavigation.Phase2SelectMusicSceneChapter9Phase1Control.<ToPhase2>…(LoadSceneAsync)
Chapter9Puzzle字面量表Chapter9Phase2Control.<EnterPuzzle>d__99
DeductionBaselineCourseLevelMod.DeductionSceneNameC9DeductionControl.<FinishAndWhiteOut>d__76
LevelJumpToTargetSession.LevelScenePath 末端名C9S6Unlock.<EnterLevel>、ChallengeModeControl.<EnterLevel>、Chapter9SongLevelStartControl.<StartAnimation>、DesultorySignalsIntroControl.NextScene()、Chapter9PuzzleControl.<PlayNextStage>…(12 个类共用)
SaturnOS / BootScene / Setting / SplashScene*字面量表 idx 7795 / 2493 / 7936 / 8124…启动器与收藏馆

第九章切场景的完整链路:Chapter9SelectMusic →(ToPhase2)Chapter9Phase2 →(EnterPuzzle)Chapter9Puzzle →(FinishAndWhiteOut)Deduction →(StartNextScene)Level / 结算 → Chapter9Phase2 → … → Level(S6/Hate/DS/Message)→ Credits。


3. 第九章的全部条件#

3.1 章节解锁#

gameinfo.json · chapters[]
{ "chapterCode": "MainStory9", "banner": "Chapter 9",
"unlockInfo": { "unlockMode": 1, "unlockFlag": "DistortedFate.Sakuzyo.0" } }

Tooltip 原文:1: Lock, required specified song reaching 88w → 第八章《Distorted Fate》880,000 分。

3.2 12 首曲 = 12 个键#

曲目解锁键定数(EZ/HD/IN/AT)
About The UniverseC9AboutTheUniverseUnlocked4.5 / 9.7 / 14.4 / —
ImplexroughC9ImplexroughUnlocked5.5 / 10.2 / 15.3 / —
EvanescentC9EvanescentUnlocked6.5 / 12.6 / 15.7 / —
Entrance to the ChaosC9EntranceToTheChaosUnlocked8.8 / 13.7 / 16.8 / 17.6
Exoplanetary MirageC9ExoplanetaryMirageUnlocked8.7 / 13.4 / 16.8 / 17.9
True Home, True World (Rework)C9S6Unlocked5 / 11.9 / 15.8 / —
AmetrineC9AmetrineUnlocked6 / 12.2 / 15 / 16.2
PetrichorC9PetrichorUnlocked7.5 / 13.8 / 16.5 / 17.5
ハテ(Hate)C9HateUnlocked9 / 13.9 / 16.7 / 17.9
Desultory SignalsC9DesultorySignalsUnlocked10 / 14 / 17 / 18.0
MessageC9MessageUnlocked3 / 13.2 / 15.6 / 16.5(+ 全游戏唯一 Chart_SP)
What do you want more than a Happy ending?C9HappyEndingUnlocked0.5 / 11.5 / 16.1 / 17.1

3.3 两台状态机#

进度 Step(存档键 Chapter9Phase2Step)
Phase2Entry(0) → AwaitTrueHomeTrueWorld(1) → AwaitAmetrine(2) → AwaitPetrichor(3) → BaselineChallenge(4)
演出 RuntimeStage
None → HatePlaying → DesultoryUnlockVideo → DesultoryPlaying
→ MessagePlaying → MessageStory → HappyEndingVideo → Credits
方法RVA
Chapter9Phase2Rules.CanAdvance(current, target)0x1D1C2E8
Chapter9Phase2Progress.TryAdvance(expected, next)(内含 SaveStep)0x1D1BFBC / 0x1D1BEC0
…LoadCurrentStep()0x1D1BCD4
…TryAwaitTrueHomeTrueWorld()0x1D1A604
…TryAwaitAmetrine / …TryCompleteAmetrine(bool judgeCheckFailed)0x1D1C1C4 / 0x1D1C1D0
…TryCompletePetrichor()0x1D1C1F4
…CompleteChapterFromAboutUs()0x1D1C200
Chapter9Phase2Rules.AreKeyCollectionsComplete(requirements, isOn)0x1D1C344

两台状态机(从代码枚举生成)
两台状态机(从代码枚举生成)

两套状态为什么分开

Step(进度)决定能进什么(存档键 Chapter9Phase2Step);RuntimeStage(演出)决定现在演什么(静态属性 Chapter9Phase2Session.Stage,只有它自己的 set_Stage 能写)。前者被成绩/结算推动,后者被场景与事件推动——互不写对方的键。


4. 逻辑与判定:状态、条件、触发#

第二章讲「谁在哪一步动了手」,这一节讲判定规则本身长什么样。第九章的逻辑集中在四个类里,全是纯函数或静态会话。

4.1 会话核心 Chapter9Phase2Session(TypeDefIndex 4404)#

Chapter9Phase2Session.cs
public static class Chapter9Phase2Session {
private const int BaselineSongCount = 3; // ★ 基准课固定 3 首
private const string CourseLevelModAddress = "LevelMod/BaselineCourseLevelMod";
private const string MessageSpChartAddress = "Assets/Tracks/Message.くるぶっこちゃん.0/Chart_SP.json"; // ★ 唯一 SP 谱
public static bool IsActive { get; set; }
public static Chapter9Phase2State.RuntimeStage Stage { get; set; } // 演出阶段(private set)
public static int CurrentSongIndex { get; set; }
public static int SelectedLevelIndex { get; set; }
public static bool IsCurrentLevelPrepared { get; }
public static Chapter9Phase2Config Config { get; }
public static Chapter9Phase2Config.BaselineSongDefinition CurrentSongDefinition { get; }
public static bool TryBeginBaselineChallenge(Chapter9Phase2Config sessionConfig, int selectedLevelIndex);
public static bool TryResumeMessage(int levelIndex);
public static bool TryPrepareCurrentLevel(out LevelStartInfo levelStartInfo, int? selectedLevelIndex);
public static bool TryApplyCurrentLevel();
public static bool TryCompleteCurrentSong();
public static void Reset();
private static bool TryValidateConfig(Chapter9Phase2Config sessionConfig, int selectedLevelIndex);
private static bool SupportsLevel(SongsItem song, int levelIndex);
private static bool Fail(string message);
}
  • Stage 只有一个写入者:[CompilerGenerated] private static void set_Stage(...)(在 Chapter9Phase2Session 里)。演出阶段只能经这个会话切换,别的类改不到。
  • 两套状态分离:Stage(演出:Hate→DS→Message→HappyEnding→Credits)与 Step(进度:五步)互不写对方的存档键。
  • 关卡准备两段式:TryPrepareCurrentLevel(out LevelStartInfo, levelIndex) 先组信息,TryApplyCurrentLevel() 再应用——对应选曲页的「预览信息 → 进关」。
  • 三重闸:TryValidateConfig → SupportsLevel(song, levelIndex)(这首歌支不支持该难度)→ 失败 Fail(message)(即 Song {0} cannot complete while the session stage is {1}.)。
  • Message 的 SP 谱走硬编码路径(不走常规难度索引),基准课固定 3 首并挂 BaselineCourseLevelMod。

4.2 判定纯函数 Chapter9Phase2Rules#

public static bool IsAmetrineConditionSatisfied(bool judgeCheckFailed);
public static bool AreKeyCollectionsComplete(IReadOnlyList<CollectionRequirement> requirements, Func<string,int,bool> isUnlocked);
public static bool CanAdvance(Chapter9Phase2State.Step current, Chapter9Phase2State.Step target);
  • CanAdvance 是 TryAdvance 的第一道关;IsAmetrineConditionSatisfied 的入参是 bool:Ametrine 必须把「挑战是否失败」传进来,两种结果都能推进但语义不同。
  • AreKeyCollectionsComplete 把「收藏要求」与「是否解锁」解耦:判定函数不改状态,只回答是/否。

4.3 每首曲的附加条件(unlockInfo 数据原文)#

12 首曲默认全开(unlockType = 0),唯一例外:

gameinfo.json · Desultory Signals · AT
// Desultory Signals · AT
"difficulty": 18.0, "levels": ["EZ","HD","IN","AT"],
"unlockInfo": [ …, { "unlockType": 2, "unlockInfo": ["Chapter9Phase2Passed"] } ]

→ AT 难度要等「二阶段已通关」(Chapter9Phase2Passed);这是全章唯一的难度级门槛。

同一张 Key 表里还有两个不是曲目的解锁 id(原文):

常量值用途
RandomKeyRandom.SobremSilentroom.0「随机曲」抽取用的占位 id
DoppelgangerKeyDoppelganger.LeaF.0「打 Doppelganger」这一解谜步骤的钥匙(与 DoppelgangerLevelEffect 呼应)

4.4 LevelMod 清单(global-metadata 字面量)#

LevelMod/001UiChange ← 第一形态 UI 变更 LevelMod/100UiChange ← 第二形态 UI 变更
LevelMod/AmetrineLevelMod LevelMod/PetrichorLevelMod
LevelMod/BaselineCourseLevelMod LevelMod/ChallengeModeLevelMod
LevelMod/DesultorySignalsLevelEffects LevelMod/DoppelgangerLevelEffect
LevelMod/HateLevelEffects LevelMod/MessageLevelEffects
LevelMod/SecretChallengeLifeMod LevelMod/Qzk
LevelMod/DFLevelEffect LevelMod/Destruction321LevelEffect LevelMod/LuminescenceLevelEffect
LevelMod/RetributionEffect(SecondPhase) LevelMod/RrharilEventEnter LevelMod/TheChariotLevelEffect

第九章是「一首曲一个 LevelMod」;第八章的 DF / TheChariot / Luminescence / Retribution / Destruction321 在同一张表里(4.0 的关卡系统共用)。

4.5 噪域的触发条件(结论来自代码)#

噪域由哪几首曲的 LevelMod 拉起
噪域由哪几首曲的 LevelMod 拉起

噪域不是全局开关,而是由曲目 LevelMod 在后处理链上按时间轴推:

曲目驱动者字段
True Home, True World (Rework)(S6)C9S6LevelEffectAnimationDirector vignetteNoiseBrightness (0x78)、vignetteNoiseSaturation (0x80)
ハテ(Hate)HateLevelEffectsvignettePlus + AnimationDirector vignetteNoiseBrightness (0x90)
MessageMessageLevelEffectsvignettePlus(+ lensDistortion / glitch / bloom / lightBand)
(对照)第八章 Distorted FateDFLevelEffectVignettePlusSettings vignettePlus + AnimationDirector noiseBrightness/noiseSaturation

效果本体:

VignettePlusSettings.cs
public class VignettePlusSettings : PostEffectSettings {
public Vector2 center; public float smoothness, radius, darkness;
[Range(0,10)] public float noiseBrightness; // ← 被上面的 AnimationDirector 驱动
[Range(0,10)] public float noiseSaturation;
[Range(0,1)] public float lineOpacity;
[Range(0.1,1)] public float noiseSampleRate; // 采样率(颗粒粗细)
private static RenderTexture _noise; // 全局噪声图
}
public class GlitchDogSettings : PostEffectSettings { // “近亲”:线噪
public float noiseFrameCount, noiseMoveSpeed, noiseEvolveSpeed;
public Vector2 noiseScale, noiseOffset;
private Texture2DArray UniversalWireNoiseTexArray, AndroidWireNoiseTexArray;
}

结论:噪域 = VignettePlus 的 noise 通道(+ 可选 GlitchDog 线噪),由该曲 LevelMod 的 AnimationDirector 在关卡时间轴上拉起;玩家侧的课题条件「不因噪域的影响而产生失误,并达到 A 等成绩」(四语言原文)就是把这条曲内演出写成通关要求。课题档案「破译·噪域 / [Decipher] Noise Field」是它的说明页。

4.6 档案(收藏品):44 条与它们的分类#

CollectionDatabase(sharedassets22.assets,path_id 143)里第九章共 44 条(items[459]–[502])。每条带 key / subIndex / 标题(六语言)/ date / 分类 / supervisor:

分类条目
mainm9beginning(D.O.M.E.)、qiongdingguzhou(行与憩)、liangrenhuihe(收获)、blackhole(答案 / The Answer)
keynizhidaoma#2–4、poyidomejiu#1–2、poyidomegino#1–2、guguthinking3、thechaos、thenoise(破译·噪域)、thewall、thewinner、themessage、thelier(现实)
boldthemirage(系外幻象)、sundemimi#1–4(隼的笔记本)、guguthinking1/2
souvenirdomeabout1–6(关于:PhigrOS / 像素塔 / 林泊 / 穹顶 / 幽蓝边界 / 收藏品)
nonsensenizhidaoma#1(归零 / Rewind)、heimu1#1–2、unknownsignal1–10

日期也很说明问题:key/main 类几乎全是 773.A.P.??.??(塔内纪年),而 heimu1 是 2091.08.31 / 2092.02.20、thelier 是 2091.08.31——现实侧与塔内纪年是两套时间轴。

关于「解锁条件」的说法要谨慎

我先前在字面量/文本表里看到 C9*Unlocked 与档案键相邻,据此推断过「某档案由某曲的键解锁」。但那张表存在列错位(见剧情篇的订正说明),且 CollectionItem 结构里并没有可直接读出的「解锁表达式」字段。所以:「thenoise 由 C9PetrichorUnlocked 解锁」这类配对,目前只能算线索,不算结论;可靠的部分是上面的 key / 分类 / 日期 / 归属章节。

5. 新元素:噪域(Noise Field)#

玩家可见条件(四语言原文):

简:不因噪域的影响而产生失误,并达到A等成绩
繁:不因噪域的影響而產生失誤,並達到A或以上成績。
日:ノイズフィールドの影響でミスをせず、A以上のランクを達成しなさい。
EN:Do not make mistakes due to the influence of the noise field, and achieve a rank of A or above.

实现层是「噪声纹理 + 后处理」:

颜色与「听觉」也是它的一部分
  • 颜色:VignettePlusSettings.ShaderIDs 里有 _NoiseCol(还含 _NoiseBrightness / _NoiseSaturation / _LineOpacity / _Center / _Smooth / _Radius / _Darkness)——噪声是带颜色的,实机上这一层是红色;设置类里只暴露亮度/饱和度/采样率/线透明度,颜色在 shader 属性(材质)里。
  • 低通有先例:AudioLowPassFilter.set_cutoffFrequency 被用在 Chapter9Phase1Control / Chapter9Phase2Control 的 DuckBgm / FadeInBgm / RestoreBgm(BgmDuckingCutoffFrequency = 1000、恢复 BgmOpenCutoffFrequency = 22000、音量 BgmDuckingVolume = 0.6)以及 LevelControl.<Start>d__46——“把声音闷住”是这一章惯用的手法。
  • 边界:C9 的 LevelMod 类里没有 lowpass 字段,所以”噪域期间音频被低通”目前算实机现象 + 近似实现(演示按 1000Hz 低通 + 红色噪点还原)。
VignettePlusSettings.cs
public class VignettePlusSettings : PostEffectSettings {
public Vector2 center; public float smoothness, radius, darkness;
[Range(0,10)] public float noiseBrightness; // 亮度
[Range(0,10)] public float noiseSaturation; // 饱和度
[Range(0,1)] public float lineOpacity;
[Range(0.1,1)] public float noiseSampleRate; // 采样率
private static RenderTexture _noise; // 全局噪声图
}
  • 动画驱动:MessageLevelEffects / C9S6LevelEffect(AnimationDirector vignetteNoiseSaturation)、DFLevelEffect(noiseBrightness / noiseSaturation)。
  • 档案 thenoise 的标题即 「破译·噪域 / [Decipher] Noise Field」;世界文本里「真理之珠总带着断断续续的噪声」。

噪域档案
噪域档案


5.1 噪域会不会「取消判定」?(把边界说清)#

先给结论:在客户端里,噪域不碰判定逻辑

把四个带噪域的 LevelMod 全字段过一遍(C9S6LevelEffect / HateLevelEffects / MessageLevelEffects / DFLevelEffect)——里面只有后处理设置、粒子、材质、UI 遮罩,没有任何 JudgeControl / JudgeLineControl / Note 引用;噪点本体也只是 Texture2D(NoiseBg*)与 VignettePlusSettings 的后处理通道。所以「噪域」本身不会修改判定结果。

但你说的「会取消判定」在谱面层是真事,只是它不叫噪域,而是下面两套机制:

① 判定线会「消失」:谱面格式里每根线都带 judgeLineDisappearEvents,值是 1 ↔ 0 的插值段——1 = 正常显示,0 = 判定线从画面上消失(注意:是显示消失,音符照常判定)。第九章用得非常凶:

谱面判定线消失次数(1→0 段)
Exoplanetary Mirage · AT673
Desultory Signals · IN655
ハテ · AT592
Desultory Signals · AT558
Evanescent · IN552
Ametrine · AT538
True Home, True World · IN398
Ametrine · IN379
…(全章合计)7,848

② 「幽灵音符」(不参与判定的音符):结算数据 LevelResultInfo 里有一个专门字段:

LevelResultInfo(结算数据)
public int perfect, good, bad, miss; // 四类判定结果
public int early, late; // 两类时相
public int maxCombo;
public Dictionary<int, bool> ghostNoteJudged; // ← 每根判定线上的“幽灵音符”是否被判到

也就是说:客户端把「某些音符不参与判定/被单独记录」这件事写进了结算结构——这才是「取消判定」在数据里的样子;它属于谱面与演出层,不是噪域这个后处理效果。

③ 判定「皮肤」是可以换的(这大概是你记得的「几种状态」):

UiChange.cs(LevelMod/001UiChange、LevelMod/100UiChange)
public GameObject Click, Drag, Hold, Flick; // 四种音符的显示对象
public Sprite ClickHL, HoldHL0, HoldHL1, DragHL, FlickHL;
public GameObject perfectJudge, goodJudge; // 判定提示字样也能被替换

UiChange 是纯换皮 mod:OnEnable 时把音符贴图与 Perfect / Good 判定提示换成另一套。第九章两处 UI 变更(001UiChange、100UiChange)就是两套皮肤——同一个判定,视觉上可以长得不一样。

还没确证的部分

「3 种状态」如果指的是判定结果的档位,客户端里是 4 类结果(perfect / good / bad / miss)+ 2 类时相(early / late);如果指的是音符种类,则是 4 种(Click / Drag / Hold / Flick)。噪域与判定的任何直接耦合(例如「噪域期间判定失效」)在客户端代码里没有找到路径,若要下这个结论需要实机验证——目前只能说:噪域影响的是”看不看得清”,而不是”判不判得到”。

附:噪域触发 / 不触发的对比演示(用游戏内的 Message 曲目与包内贴图重构,非录屏)。强度分层:0–40% 红线噪 + 暗角 → 40–80% 叠加 glitch 方块纹理 → 80–100% 方块纹理满强度 + 水平切片错位(RGB 偏移);用到的包内纹理为 GlitchMap / Glitch4in1 / BlockNoise1 / NoiseBgLines / FD_Noise_00000(对应 GlitchSettings.glitchTex 与 GlitchDogSettings 的线噪阵列):

「不触发」与「触发」单版本(同一段音乐;触发侧是红色噪点、音频做了低通,所以两边听感不同):

6. 选曲界面怎么换成第二形态#

机制证据
形态标记Chapter9LastSelectMusicIsPhase2
两套封面#ChapterCover/MainStory9.jpg / MainStory9_2.jpg
进入Chapter9Phase1Control.ToPhase2() → TryBeginSecondPhase() + PlayerPrefs.Save() + LoadSceneAsync
首次FirstEnterPhase2Control.OnFinished → TryAdvance + PlayerPrefs.Save;超时 [ToPhase2] Video timed out.
启动器SaturnOSLaunchControl(statusText/L/R/Err、progressBarProgress、loadingSlide、toCollectionAnim)
里程碑Chapter9UnlockBegin / Chapter9UnlockSecondPhaseBegin / Chapter9Phase2Passed

第二形态封面
第二形态封面


7. LIFE 挑战(秘密挑战)#

LIFE 档位逻辑:10/50/100/200、Good 扣命分界线与晋升/应用/领取
LIFE 档位逻辑:10/50/100/200、Good 扣命分界线与晋升/应用/领取

SecretChallengeLifeRules.cs
public static class SecretChallengeLifeRules {
public static readonly int[] LifeTiers; // ← 实数值 10 / 50 / 100 / 200
public const int InitialUnlockedTierIndex = 1;
public static int GetLifeForTierIndex(int);
public static int GetSavedTierIndex(); public static int GetStartingTierIndexAfterPreviousUnlock(int);
public static void SetUnlockedTierIndex(int); public static int GetSelectedTierIndex(); public static void SetSelectedTierIndex(int);
public static bool DeductsLifeOnGood(int t); // == GetLifeForTierIndex(t) < 100
public static void PromoteAfterFailure(GameInformation); // 失败 → 晋升
public static void ApplyAfterSuccess(GameInformation); // 成功 → 应用
public static bool TryConsumePendingUnlock(out int); // 领取“待解锁”
public static void BeginRun(); public static void RestartRun(); public static void ClearSaved();
}

LIFE 档位徽章:life1 → life200(右侧为逐帧红噪动画的抽样帧)
LIFE 档位徽章:life1 → life200(右侧为逐帧红噪动画的抽样帧)

这些数值是实打实读出来的

LifeTiers 初值在 global-metadata.dat 偏移 0x5BAC1C:0A 00 00 00 32 00 00 00 64 00 00 00 C8 00 00 00 = 10 / 50 / 100 / 200,由 SecretChallengeLifeRules::.cctor()(RVA 0x1D237C8)的 RuntimeHelpers.InitializeArray(0x1D2383C)装载。 「Good 也扣命」的判定就一句 GetLifeForTierIndex(t) < 100(RVA 0x1D22EC8,cmp w0,#0x64)→ 10/50 档扣,100/200 档不扣。关卡内每次失误 SecretChallengeLifeMod.OnNonPerfect() 里 sub w8, w8, #1(RVA 0x1CE6B68,扣 1);扣到 0 时调 PromoteAfterFailure(@0x1CE6BBC)。

两处存疑,别当结论

① LifeTiers 前 4 字节恰是 01 00 00 00,所以「数组其实是 {1,10,50,100,200}」不能排除(那样 index 1 = 10 血,与社区「首档 10 血」自洽)。② 社区说「Bad/Miss 扣 2」——客户端只确证了 -1 这一处,扣 2 可能由判定/结算模块处理。

  • 存档键:C9SecretChallengeLifeTier / -SelectedLifeTier / -PendingLifeUnlock。
  • 关卡内:SecretChallengeLifeMod——Start() → SetComboDisplayOverride + EnableTrueShadow(接分数事件),OnNonPerfect() → 红闪 + FlashLifeText() → FadeLifeTextWhiteToBlack,AudioFadeOut;SecretChallengeLifeBar 需 Level UI 相机 + RawImage + 材质模板。
  • 选曲/挑战页:LifeBadgeControl(lifeSprites 按档位;SwitchLifeTier()、GetLocalizedHint(tier)、PlayOverlayFade();静态 NonPerfectHint / BadMissHint)、LifeUnlockControl(Start 读本地化文案;Show(int) → TrySetImage;DismissLockSeconds = 3 防误触)。
  • 流程:选档位 → 提示 → 打关(扣命 / 晋升 / 应用)→ 解锁展示 → 领取(Pending → Tier)。

8. 课题界面:那些「乱码」是设计好的#

private const int DummyPoolSize = 100;
private const int DummyTextMinLength = 8;
private const int DummyTextMaxLength = 12;
private const string DummyChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
public float dummyTextInterval; public Text[] dummyTargetTexts;
private void BuildDummyTextPool() → CreateDummyText(...) // 先把版面填满随机串
public void PlayUnlock() → AudioSource.Play
private static readonly int SecretSongRequirementMet; private static readonly string[] UnlockSongIds;
  • 乱码是占位态:dummyTargetTexts 按 dummyTextInterval 刷 8–12 位随机字母数字;满足秘密条件后才替换真数据。<…>MoveNext 最后 LoadScene。
  • 配套:ChallengeStartReveal(reveal + secretLayerName)、ChallengeStartBackgroundUv(BaseMeshEffect 改 UV)、Chapter9CollectionGlitchRemover、场景 ChallengeMode / ChallengeModeOver。

9. 密码与秘密包:算法、加载、卸载#

密码界面:ACCESS DENIED + 输入框 + 提交键
密码界面:ACCESS DENIED + 输入框 + 提交键

9.1 判定链#

密码 → 密钥派生 → 解密:SHA-512 切片成 Key/IV,明文头 UnityFS 即成功
密码 → 密钥派生 → 解密:SHA-512 切片成 Key/IV,明文头 UnityFS 即成功

Submit() [0x1D486FC] → SetPassword(string) [0x1D26B70]
SHA512.Create → Encoding.UTF8 → ComputeHash → GetSubArray<byte>(0..32 / 32..48)
→ aes.Key / aes.IV → Addressables.LoadAssetAsync<object>(...) // 重载
→ <Submit>b__3 → FadeToBlackThenStartC9S6() [0x1D48B34] → SceneManager.LoadScene
环节值
KDFSHA-512(password)(64B,无盐)
Key / IVdigest[0:32] / digest[32:48]
模式AES-256-CBC + PKCS7(.NET Aes.Create() 默认)
成功判据去填充成功 / 明文头 UnityFS(双重自校验)

坑:部分包首 16 字节是独立 IV,直接当密文解会坏首块;稳妥做法是按 find(b'UnityFS') 对齐。

9.2 秘密包的运行时生命周期(Addressables 侧)#

Provide(ProvideHandle) [0x1D27850]
└─ C9SecretAssetBundleResource
GetLoadInfo / BeginOperation
├─ LocalRequestOperationCompleted() // 本地
├─ WebRequestOperationCompleted() // 远程(BytesToDownload / GetDownloadStatus)
├─ GetEncryptedAssetLocalPath(name) // 密文路径
├─ <LoadLocalAssetBundle>d__36.MoveNext → Decrypt(Stream) [0x1D271D4] → Task<MemoryStream>
└─ WaitForCompletionHandler → AssetBundleUnloadOperation.WaitForCompletion()
Release(IResourceLocation, object)
└─ C9SecretAssetBundleResource.Unload(out op) → AssetBundle.UnloadAsync(false)

三点值得记:

  1. 只有走这个 provider 的资产是密文,其余包走标准 AssetBundleProvider;
  2. provider 支持远程下载(BytesToDownload / GetDownloadStatus / WebRequestOperationCompleted)——秘密资产既可从包内、也可从 CDN 取;
  3. 卸载走 UnloadAsync,unloadingBundles 字典防重复卸载,与 WaitUntilNextUpdate / RegisterUpdateReceiver(RuntimeInitializeOnLoadMethod)配合「等下一帧再收」。

9.3 8 个包里有什么#

包大小内容
c9s.MYy0ioG21.0 MBAudioClip: music(68.51 秒秘密曲)
c9s.Qf9C6hd313.6 MBC9S6 剧情场景 + VideoClip: LoopBackground(9s, 1920×1080)+ 黑洞音效
c9s.ilBx0rGL645 KBTextAsset: Chart(458 音符)
c9s.U8WX0Xsd70 KBIllustrationBlur
c9s.sXroRzXR557 KBTrueHomeTrueWorldVignettePlusSettings、Exposure_Saturate_ContrastSettings、Glitch4in1
c9s.vK7mQp2H3.8 MBCollectionHeader
c9s.zFzUbVcZ6.0 MBCollectionLocked + 字体/材质
c9s.test1.2 KBTextAsset: test(内容 pass)

收藏页头图(CollectionHeader)
收藏页头图(CollectionHeader)


展开:68 个脚本类各自干了什么(点击展开)

每条都来自调用图(c9_callgraph.json);→ 后是它实际调到的关键 API。

入口与阶段

  • LaunchControl — ToChapter9() 保活控制器并选 Phase1/2 协程;EnterPhase1/2Coroutine 里 DontDestroyOnLoad、淡出 BGM(AddressableAudioSource.Volume / CancelFadeIn)、Destroy 旧对象。
  • Chapter9Phase1Control — 选曲页总控:Start() 起协程 + 切 BGM(StartBgmTransition)、PlayUnlockSE / PlayLinkSE、ToPhase2() → TryBeginSecondPhase + PlayerPrefs.Save + LoadSceneAsync;OnDestroy → ReleasePhase1EndingAmbient。
  • Chapter9Phase2Control — 二阶段总控:Start → TryBeginSecondPhase + ApplyMapCover;ToSaturnOS / ToSettings / Back 走协程;<EnterPuzzle…> / <TurnToNextScene…> → LoadScene。
  • FirstEnterPhase2Control — 首入二形态:挂 VideoPlayer.prepareCompleted / loopPointReached,OnFinished → TryAdvance + PlayerPrefs.Save;OnDestroy 解绑。
  • Chapter9Phase2Config — baselineSongs + CreateBaselineCourse()(基准课表:BaselineSongDefinition{songId, levelModAddresses})。
  • BaselineChallengeStartControl — 基准课入口:SetLevel(int)、Play()、RefreshVisibility() → LoadCurrentStep()。

状态机与集合

  • Chapter9Phase2State — 两个枚举:Step / RuntimeStage。
  • Chapter9Phase2Progress — TryAdvance(内含 LoadCurrentStep + SaveStep)、TryAwait* / TryComplete*、CompleteChapterFromAboutUs。
  • Chapter9Phase2Rules — 三条纯函数:CanAdvance / IsAmetrineConditionSatisfied / AreKeyCollectionsComplete。
  • Chapter9Phase2Session — 会话层:TryBeginBaselineChallenge、TryResumeMessage(都先 LoadCurrentStep)。
  • Chapter9Phase2Effect — OnEnable() → PostProcessingManager.AddEffects(进关卡挂后处理,Update 起协程/动画)。
  • CollectionControl — DisplayCanvas()(展示收集品画布)。
  • CollectionRequirement — {collectionKey, collectionSubIndex} 的可序列化条件。

谜题 / 推演 / 剧情文本

  • Chapter9PuzzlePiece — Awake() 设 alphaHitTestMinimumThreshold = 0.2;TryEndDrag() → StartCoroutine(吸附判定)。
  • Chapter9PuzzleControl — OnPieceCompleted()(完成音效 + TweenCompletePostEffects)、PlayShowPopupExposure()、ConfirmNextStage()、<PlayNextStage…> → TryBeginBaselineChallenge + LoadScene。
  • Chapter9PuzzlePopupControl — Awake() 取 LocaleText;TextReplacement 协程做文本置换。
  • C9DeductionControl — 推演:PrepareVisualLayers(CreateVignetteTexture / Sprite.Create)、RenderPage(析构旧行)、UpdateTypingText;Awake → TryCompleteCurrentSong;<FinishAndWhiteOut…> → LoadScene;OnDestroy → StopAllCoroutines + DestroyRuntimeVisuals。
  • C9AfterDeductionControl — 后日谈:VideoPlayer.Prepare/Play、PlayerPrefs.Save、<StartNextScene> → LoadScene。
  • C9AfterDeductionCaptionControl — 字幕:VideoPlayer.get_time 驱动 4 组字幕条件 + LocaleText。
  • Chapter9StoryTextView — 通用剧情文本视图:Play / Advance / StopPlayback、ShowPage(CreateTextLine)、StartTypingNextLine。
  • C9StoryControl(秘密包内) — 终章演出:Start 配 VideoPlayer(isLooping / audioOutputMode / Play)、RenderPage / UpdateTypingText、<TurnToNextScene> → LoadScene、OnDestroy 清理。
  • C9StoryPostEffects(秘密包内) — 以 Cue 编排后处理:EnsureDefaultCues、InstallEffects(AddEffect / CaptureState / PushCompositeToRuntime)、DestroyRuntimeEffect。

密码与秘密资产

  • Chapter9InputPasswordControl — Open() 取本地化标题;Submit() → SetPassword;失败走 RunWrongPasswordOptics / RunChangeColor;PlayPasswordSeClip(DontDestroyOnLoad 音效)。
  • C9SecretAssetBundleProvider — §8.2 的 Provide / Release / SetPassword / Decrypt / EncryptAndWrite、RegisterUpdateReceiver。
  • Chapter9LevelsSettings — PostEffectSettings 子类(inBlack / inWhite / gamma / outBlack / outWhite),白屏演出的参数源。

选曲 / 地图 / 收藏 / 详情

  • Chapter9MapOverview — Enter() / Exit()(进/出地图,协程做相机移动)。
  • Chapter9MapElement — 三态 + Sync()(同步状态、FadeInLockedScanEffect)。
  • Chapter9MapElementLink — 连线/填充:<FillRoutine…> → AddMapFocus / RemoveMapFocus。
  • IChapter9MapFocusProvider — 地图聚焦接口(与上两者互调)。
  • Chapter9UnlockNavigation — 解锁跳转;Chapter9SongUnlockSprite — Awake() → SyncRaycastPadding + 换 sprite。
  • C9SecretCollectionControl — 秘密收藏点击域(六边形命中)。
  • Chapter9CollectionUnlockSprite — UpdateDisplay → UnlockAnimation → ApplyUnlockedAppearance,并把地图聚焦交回 Chapter9Phase1Control。
  • Chapter9CollectionGlitchRemover / Chapter9CollectionDetailsControl / …IOLSizeControl — 解锁后消故障、收藏详情、插画尺寸自适应。
  • Chapter9SongDetailsControl — Open / Close / UpdateInfo(含 LoadDifferentCoverImage:不同难度不同曲绘)。
  • Chapter9SongDetailsLockedControl / …IllustrationSizeControl — 锁定态文案、曲绘尺寸。
  • Chapter9SongLevelStartControl — 开谱:PrepareBackground(截图 + RT + DontDestroyOnLoad)、DestroyBackground(RenderTexture.Release + Destroy)、<Start…> → LoadScene。
  • Chapter9FakeChallengeSongSelector — §7 的乱码池 + 秘密替换 + 白屏 GlitchSettings / Chapter9LevelsSettings。

演出 / 转场 / 特效

  • DesultorySignalsIntroControl — §2.3 的六路视频;ClearVideoRT() 释放 RT;NextScene() → LoadScene。
  • DesultorySignalsIntroCanvasScaller — 该场景的 Canvas 缩放校正。
  • C9S6Unlock — TryBeginFadeInFromBlack() + EnterLevel()(RawImage.set_texture → LoadScene)——点下去就是 S6。
  • DSUnlockControl / ExoplanetaryMirageUnlock — 解锁演出(后者含 SetMirrored / SetTextMirrored 镜像文字)。
  • HateGlitchEffect — Start() → LoadCurrentStep() 决定故障档位。
  • ImplexroughRandomGlitch / Chapter9EntranceToTheChaosGlitch / Chapter9PoyidomejiuAltGlitch — 曲目专属故障(最后一个 OnDisable 会 Destroy 临时 sprite)。
  • DoppelgangerLevelEffect — CreateVignette(Image.set_sprite/type/preserveAspect)、ApplyWeight、TryLoopPlayback(AddressableAudioSource.Seek)、OnDestroy → Destroy。
  • DesultorySignalsLevelEffects / HateLevelEffects / MessageLevelEffects — Start → AddEffect、HideUIElements、Update 按音频进度 AnimationDirector.Evaluate(DS 另有 UpdateSlashInfo / ApplySlash)。
  • AmetrineLevelMod / PetrichorLevelMod — Ametrine 走 PostProcessingManager.AddEffects + 协程;Petrichor 是纯参数 Mod。
  • C9S6LevelEffect — 秘密包内 S6 特效:Awake 里 Destroy / DontDestroyOnLoad 双轨、Start → AddEffect、Update → Evaluate。
  • BlackScreenTransition — BeginTransition(sceneName, …) 里 DontDestroyOnLoad,<TransitionRoutine…> → LoadSceneAsync。
  • CutInAnimator / TriFrameCutOutAnimator — 切入 / 三角框切出动画驱动。
  • WorldCanvasScroller — DOME 背景的世界画布滚动。
  • Chapter9ColorOverlayControl — ToBlue() / ToRed()(覆盖层变色,配密码失败演出)。
  • Chapter9WhiteoutTransition — §2.6 的白屏:截图 → Graphics.Blit → 后处理挂载 → LoadScene;Teardown 成对释放。

挑战 / LIFE UI

  • ChallengeStartReveal — 遮罩渐显(reveal + secretLayerName)。
  • ChallengeStartBackgroundUv — 背景 UV 扰动(BaseMeshEffect.ModifyMesh)。
  • LifeBadgeControl — 档位徽章:SwitchLifeTier / Refresh / PlayOverlayFade / GetLocalizedHint。
  • LifeUnlockControl — 解锁弹窗:Start 读文案、Show(int) → TrySetImage、<WaitForFadeOut>。
  • SecretChallengeLifeMod — 关卡内生命:Start → SetComboDisplayOverride + EnableTrueShadow;OnNonPerfect → 红闪 / 文字淡出;AudioFadeOut。
  • SecretChallengeLifeBar — 生命条 UI(RawImage + 材质模板 + 相机会话)。
  • SecretChallengeLifeRules — §6 的规则集。

其它 / 清单外但同属这一章的类

Key 的字段是 keyName / unlockedTimes / kindOfKey / unlockTimes;差集查出来的这些(命名空间确属第九章体系):

类它干了什么
TwoFingerPinchSession双指捏合手势会话(解谜触控判定):hasBaseline / locked / initialDistance,InchesToCentimeters = 2.54、FallbackDpi = 96,Tick(touchCount, distance, triggerRatio, minInitialDistance)、PixelsToCentimeters
BaselineChallengeStartControl基准课开始面板:config: Chapter9Phase2Config、startRoot、[Range(0,3)] selectedLevelIndex、startButton,SetLevel / Play / RefreshVisibility
SecretChallengeLifeBar生命条:shader 参数 LifeId / NoiseSpeedId / EvolutionOffsetId / TurbulenceOffsetId / TurbulenceFlowId / TurbulenceFadeId,target: RawImage、materialTemplate、GetNormalizedLife(GameInformation)——注意它有 NoiseSpeedId(生命条本身也吃噪声贴图)
LifeBarCanvasFit生命条画布适配:ReferenceHeight = 1080、MaxAspect = 1.7777778、ReferencePixelsPerUnit = 100
ChallengeModeLevelModControl课题模式的关卡 Mod 装载
ChallengeModeOverControl课题结算(字段里出现 C9HiddenSongUnlockKeys)→ <TurnToNextScene> 切场景
PopupControl / PopupEventHandler / VisionReplayButtonSaturnOS(收藏/资料馆)的弹窗基类、事件桥与「视觉回放」按钮
JumpToTargetSession + *SessionBootStrap退出后重进指定课题会话(§2.9)
BootSceneControl冷启动读 C9PendingMessage* 决定是否直接进 Message 关卡
ProgressControl.LevelBreak(nextScene, Func<string>)关卡结束 → 下一场景的决策入口(ResolveNextScene 委托挂在这里)
LevelOverControl(含嵌套 C9AboutTheUniverseUnlockConfig)结算时处理 C9AboutTheUniverseUnlocked 等解锁
GameInformation / GameProgressSaveModule / LevelControl / SaturnOSControl全局存档、关卡与收藏库主控(钩子)

10. 声音:这一章的“声音”是在什么时候、以什么方式变的#

先给结论

第九章的音频不是「一首 BGM 从头放到尾」:音乐会被交给画面、会被低通滤波器按进水里、会在特定时刻被单独拉掉一条音轨、还会被设为跨场景对象;而关卡特效反过来读音乐的播放状态来驱动画面。下面每个例子都给到字段名/RVA/资源名。

10.1 骨架#

秘密终章场景的音频时间线(三条音轨 + 亮度阶梯)
秘密终章场景的音频时间线(三条音轨 + 亮度阶梯)

#事件客户端行为证据
1启动套用全局音频配置GameInformation.Awake() → AudioConfiguration.GetConfiguration()/Reset(config)
2进第九章用独立于动画时长的 audioCutOutDuration 把章节选曲 BGM 音量降到 0;先 CancelFadeIn 再逐帧 set_VolumeLaunchControl.ToChapter9()(0x1D0B28C)、<EnterPhase1Coroutine>d__8 局部字段 5__3/5__4;callee 0x1DB56EC(CancelFadeIn)、0x1DB5500(set_Volume)
3Phase 2 切换改用全局总线:读 GameInformation.get_ListenerVolume → set_ListenerVolumecallee 0x1CB7B1C / 0x1CA58CC
4打开详情/收藏/密码框DuckBgm:同一个 BGM 同时被降音量 + 低通滤波DuckBgm(0x1CFBA54)→ AudioLowPassFilter.set_cutoffFrequency(0x3C12E54)+ AudioSource.set_volume(0x3C11410);常量 BgmDuckingVolume = 0.6、BgmDuckingCutoffFrequency = 1000、恢复值 BgmOpenCutoffFrequency = 22000;返回 Back() → RestoreBgm()
5选曲 UI 点击解锁/连线/进入音PlayUnlockSE / PlayLinkSE / Chapter9CollectionUnlockSprite.UnlockAnimation / Chapter9FakeChallengeSongSelector.PlayUnlock → AudioSource.Play
6难度确认6 路视频(CG / 选难度 / EZ·HD·IN·AT)自带音轨DesultorySignalsIntroControl 的 introCG / diffSelect / ez / hd / ins / at
7关卡内画面跟着音乐走HateLevelEffects / DesultorySignalsLevelEffects / MessageLevelEffects.Update()(0x1D460F8 / 0x1D43B64 / 0x1D469EC)唯一音频 callee = AudioSource.get_isPlaying(0x3C11A80)→ 喂给 glitchRange 等 GlitchSettings 参数
8LIFE 挑战扣命拉掉一条音轨 + 生命数字由白转黑SecretChallengeLifeMod.OnNonPerfect()(0x1CE6A90)→ AudioFadeOut(...)(0x1CE6C84)→ <AudioFadeOut>d__14 只做 get_Volume/set_Volume 线性到 0;同时 FadeLifeTextWhiteToBlack()(0x1CE6A08)
9拼图最后一块音效对拍在音频时钟上(不是帧上)Chapter9PuzzleControl.OnPieceCompleted()(0x1D3C2F0)→ AudioSettings.get_dspTime + AudioSource.PlayScheduled
10白屏过渡故障音=跨场景对象,音量直接绑故障权重Chapter9WhiteoutTransition.PlayOut(0x1CFB8D4)→ CreateSession(0x1D053D0)→ BeginGlitchAudio(0x1D063B8);每帧 Present()(0x1D05A1C)→ SetGlitchAudioVolume(weight)(0x1D05C2C),内部调 Object.DontDestroyOnLoad(0x3C5E5F0);PlayIn 收尾 EndGlitchAudio(0x1D064A8);常量 PeakGlitchRange = 0.2
11密码提交正确/错误两条 clip;正确的那条跨场景活下来字段 correctPasswordClip(0xD0) / wrongPasswordClip(0xD8) / passwordSe(0xE0)(dump.cs:345502);PlayPasswordSeClip(..., persistAcrossScenes)(0x1D48D80)→ AudioSource.PlayOneShot(clip, volumeScale)(0x3C11834)+ set_spatialBlend(0x3C11D80)+ DontDestroyOnLoad;随后 <Submit>b__3 → Chapter9Phase1Control.FadeOutBgmOnLeave()(0x1CFCB20)→ FadeToBlackThenStartC9S6()
12解开后进关拉主音量再切场景<FadeToBlackThenStartC9S6>d__39.MoveNext() → GameInformation.set_ListenerVolume(...) + 延时 + SceneManager.LoadScene
13循环类曲目每轮主动把播放位置拽回去DoppelgangerLevelEffect.TryLoopPlayback()(0x1D45870)→ AddressableAudioSource.Seek(float)(0x1DB5B90)

10.2 三个“一等例子”#

DuckBgm:打开面板时 BGM 被低通滤波器「按进水里」
DuckBgm:打开面板时 BGM 被低通滤波器「按进水里」

例 A · 打开详情时,BGM 是「被按进水里」

DuckBgm 不只降音量:它同时把 AudioLowPassFilter.cutoffFrequency 从 22000 Hz 压到 1000 Hz,音量压到 0.6。所以你在第九章点开歌曲详情/收藏/密码框时听到的「闷」不是错觉——那是低通滤波。关闭面板走 RestoreBgm 还原。

例 B · 白屏的杂音是「跨场景的」

SetGlitchAudioVolume(weight) 自己就会 DontDestroyOnLoad,音量逐帧绑在 flash 权重上(PeakGlitchRange = 0.2)。也就是说,那声「嘶——」会跟着你穿过 LoadScene,在下一幕的入场里被 EndGlitchAudio 收掉。

例 C · 视频不出声,声音是排出来的

秘密场景 c9s.Qf9C6hd3 的 VideoPlayer(LoopBackground) 把 m_TargetAudioSources 留成空数组(视频只出画);场内的声音全靠预制体硬连线:BGMPlayer → 黑洞音效(故障背景音) 16.286 s(m_PlayOnAwake = true,进场景即响)、KeySEPlayer → 黑洞音效(转场) 16.792 s(画面切换时由 visualImageSE 触发)、3 个 TypeSEPlayer → 文字浮现音效 0.140 s(打字音,按 TypeIntervalSeconds = 0.058 用 PlayScheduled + AudioSettings.dspTime 轮转)。9 条后处理 cue 全部绑在 LineTypingStarted(moment = 5),亮度从 1 一路推到 16(正好是第 13 页那句「我会来找你的!」)。

10.3 音频的容器与参数(怎么存的)#

第九章音频资产总览:25 条 DecompressOnLoad + 5 条 Streaming
第九章音频资产总览:25 条 DecompressOnLoad + 5 条 Streaming

  • 本体第九章的音频不在 Addressables 里:data.unity3d → AudioClip.m_Resource 指向 sharedassets{0,1,14,15,19,23,26,28,29,30,35,36}.resource 的字节区间,格式 FSB5(mode = 0x0F,Vorbis),全部 44100 Hz / 2 声道。
  • 第九章专属 30 条:表包氛围音乐(65.829s) / 里包氛围音乐(65.829s) / FakeAboutUs0(61.492s) / 演绎背景环境音(56.366s) / BG Loop(38.419s) / ChapterSelect0(27.429s) / Ambient_Retri(16.219s) / Ambient(15s) / 拼图拼最后一个图块(10.286s) / 拼图完成后环境音循环(6.857s) / 拼图拼单个图块(4.821s) / 地图解锁(4.228s) / openChapter9(4.2s) / 演绎转场音_4(4.152s) / Clock(4s) …
  • 导入模式混用:DecompressOnLoad 25 条(一次性音效)+ Streaming 5 条(ChapterSelect0、FakeAboutUs0、BG Loop、表包氛围音乐、演绎背景环境音)——长音频走流式,短音效解到内存。
  • 秘密包 4 条同规格;秘密曲 music = 68.5114 s。

听一下(已转码放本站):

拼图最后一块 / 地图解锁 / 时钟音效:

推演环境音(前 6 秒)与秘密场景的黑洞故障音(前 8 秒):

10.4 启动 BGM 会因为你「有没有通关第九章」而不同#

这是全章最容易被忽略、也最”纪念向”的一处:开屏音乐有两条,取决于你的存档进度。

资源时长什么时候放加载方式
NewSplashSceneBGM140.069 s未通关主线第九章时的启动 BGMStreaming(流式)
SplashScene4BGM152.329 s通关后的启动 BGMStreaming(流式)

未通关时:

通关后:

机制与边界

启动场景 SplashScene 本身就带按进度条件启用的对象列表——objectsToEnable 与 objectsToEnableC8SecondPhase(“第八章二阶段”专用),说明启动画面会按存档进度切换要显示的物件;第九章沿用同一思路(两条 BGM 也都在包里、都是 Streaming)。 边界:SplashScene 的字段里没有直接看到”选哪条 BGM”的判据(判据可能在场景里的播放器对象上),所以「通关前/后各放哪条」这一映射以实机表现为准(感谢主人的对照)。

10.5 音效一览(挑有代表性的)#

第九章的音效小而密,下面是能听出”这是第九章”的那一批:

音效时长用在哪
openChapter94.20 s进入第九章的开门音
输入密码音效2.19 s密码框(还有一条 1.93 s 的”课题组开始 / 疑似密码错误”)
地图解锁(包含蓄力)3.70 s地图节点解锁
拼图点击继续故障音3.41 s拼图阶段的白屏/故障点
拼图拼单个图块 / 拼图拼最后一个图块 / 拼图弹出确认窗口音效4.82 / 10.29 / 4.18 s拼图三连(最后一个有 10 秒的收束音)
Clock4.00 s钟表/时间类演出
演绎转场音_1~4(含”靴子""火车音效”)3.7–4.2 s推演与过场
文字浮现音效0.14 s秘密终章打字(每 0.058 s 一次)
Tap1–Tap7 / HitSong0–2 / Calibration / CalibrationHit0.10–2.0 sUI 通用点击与判定校准
展开:第九章专属音频 30 条的完整清单(名 / 时长 / 加载方式)
#AudioClip时长(s)加载
1表包氛围音乐65.829Streaming
2里包氛围音乐65.829DecompressOnLoad
3FakeAboutUs061.492Streaming
4演绎背景环境音56.366Streaming
5BG Loop38.419Streaming
6ChapterSelect027.429Streaming
7Ambient_Retri16.219DecompressOnLoad
8Ambient15.000DecompressOnLoad
9拼图拼最后一个图块10.286DecompressOnLoad
10拼图完成后环境音循环6.857DecompressOnLoad
11拼图拼单个图块4.821DecompressOnLoad
12地图解锁(包含蓄力)4.228DecompressOnLoad
13openChapter94.200DecompressOnLoad
14拼图弹出确认窗口音效4.179DecompressOnLoad
15演绎转场音_44.152DecompressOnLoad
16Clock4.000DecompressOnLoad
17演绎转场音_2_火车音效3.92DecompressOnLoad
18演绎转场音_13.69DecompressOnLoad
19拼图点击继续故障音3.41DecompressOnLoad
20输入密码音效2.19DecompressOnLoad
21Calibration2.00DecompressOnLoad
22课题组开始游戏音效 / 疑似密码错误音效1.93DecompressOnLoad
23Tap71.50DecompressOnLoad
24收集品打开1.42DecompressOnLoad
25演绎转场音_3_靴子1.29DecompressOnLoad
26openSaturnOS1.06DecompressOnLoad
27收集品关闭1.02DecompressOnLoad
28Message1.00DecompressOnLoad
29歌曲详情打开 / 关闭0.90 / 0.81DecompressOnLoad
30songInfoShow / Hide、Tap1–Tap6、HitSong0–2、CalibrationHit、文字浮现音效0.10–0.61DecompressOnLoad
(完整导出见素材馆,c9_audio/ 里另有 52 条 wav。)

10.5 两处纠正(自我否定)#

  1. Chapter9SongLevelStartControl 完全没有音频字段(33 个字段全是 Image/Text/Camera/RenderTexture),它的 DontDestroyOnLoad 是为了跨场景保留背景截图——「开谱界面播 BGM」这个假设不成立,开场 BGM 归 Chapter9Phase1Control / LevelControl。
  2. 「密码控件」的真名是 Chapter9InputPasswordControl;LifeUnlockControl / LifeBadgeControl 里的 fadeInTween/fadeOutTween 是 UI 补间,不是音频淡出。

11. 用剧情解读程序:每个子系统对应哪一幕#

这一节把两张表叠在一起看:左边的程序是客户端写死的机制,右边的剧情是它服务的叙事。剧本文本均来自游戏内档案(见剧情篇)。

程序里的东西剧情里的它证据
选曲元素三态 Locked / Unlocked / Read塔内的拣选:读过的、能进的、还没开的Chapter9MapElement.ElementState;thewall「收集……然后是拣选与摆放」
拼图(Chapter9PuzzlePiece 吸附)+推演(C9DeductionControl 打字机)把碎裂的信号拼回去、把页码一页页读出来thewinner:怪物破壳后是「几乎凝成固体的信号乱流,垒成一道无法逾越的高墙」
噪域(VignettePlusSettings 的 noise 通道 + 曲内 LevelMod 时间轴)同一批档案里就叫噪域:干扰、杂讯、够不够干净地穿过去条件文案「不因噪域的影响而产生失误,并达到 A 等成绩」;档案「破译·噪域」
白屏 / 故障(Chapter9WhiteoutTransition,故障音跨场景)幽蓝边界——像素塔的清扫程序;记忆的最终归宿nizhidaoma:「关闭像素塔……启动了像素塔的清扫程序——幽蓝边界」;domeabout5:「所有记忆的最终归宿,便是幽蓝国」
LIFE 挑战(10 / 50 / 100 / 200,DeductsLifeOnGood)她的命数:挑战越大,允许的失误越多/越少;失败反而晋升SecretChallengeLifeRules;history:LIFE 是 4.0 的秘密挑战玩法
密码(SetPassword → AES)网页 ARG 的答案句;往回走口令 Backward, go backward, …;解开的正是 S6
S6《True Home, True World (Rework)》回家:真实的世界GameInformation 键 C9S6Unlocked;终章对话
收藏品 / 档案系统(SaturnOS)收藏品 = 塔世界的记忆heimu1:「1.3.0 版本,Phigros 实装了收藏品系统……人们终于在 1.3.0 版本,记起了那个绮丽多姿的塔世界」
退出应用(BaselineCourseLevelMod.QuitForMessageResume → Application.Quit + 冷启动重放)「离开像素塔」:程序把”离开”实现成了字面上的退出,再由 Phigros.JumpTo* 把你送回去§2.9;blackhole Gino:「只要离开像素塔——一切的谜底就能揭晓了!」
JumpToTargetSession(Phigros.JumpToDS / Hate / Message)黑洞「会让你看见过去」——直接跳到指定的那段会话§2.9 的会话基建表
为什么可以这样叠

4.0 的第九章把「谜题 → 演出 → 台词」写进了同一套状态机(§3.3),所以程序里的每一次跨步,都能在档案里找到同名的一页:噪域、辟径、来信、再见、现实、答案。

12. 谱面与素材#

  • 45 个第九章谱面(charts/):EZ/HD/IN 37 + AT 7 + Message 的 Chart_SP 1;ch9_charts_summary.json 含音符数与时长(秒 = time × 60 / (32 × BPM))。
  • 美术:15 张档案、12 张曲绘、两形态章节封面、CollectionHeader。

第九章的「质感」是后处理,下面是包里的原始材质(噪域主噪点 / 线噪 / 故障图集 / 网格;完整 10 张见素材馆):

噪域主噪点
噪域主噪点
噪域线噪
噪域线噪
GlitchMap
GlitchMap
TriGrid 网格
TriGrid 网格
  • 视频(1080p → 960×540;4.0 / 4.7 / 0.2 MB):
  • 音频(点开播放):
▶ 秘密曲(68.51 s)

▶ 拼图最后一块 / 地图解锁 / Clock

▶ 推演环境音(6 s)/ 秘密黑洞故障音(8 s)

13. 复现命令#

Terminal window
unzip -j Phigros_4.0.1_TapTap.apk.1 \
assets/bin/Data/Managed/Metadata/global-metadata.dat \
lib/arm64-v8a/libil2cpp.so assets/bin/Data/data.unity3d assets/aa/catalog.json -d apk_extract
DOTNET_ROLL_FORWARD=Major dotnet Il2CppDumper.dll apk_extract/libil2cpp.so apk_extract/global-metadata.dat dump_out
python3 parse_gameinfo.py # 章节 / 歌曲 / 解锁键 -> gameinfo.json
python3 gen_c9_code_and_conds.py # 68 脚本 / 242 类 + c9_conditions.json
python3 c9_callgraph.py # ★ 本文调用图 -> c9_callgraph.json
python3 scan_c9_text.py && python3 gen_c9_texts.py # 六语言文本

:::: code-group labels=[Python · 解密秘密包, Bash · 反汇编指定方法]

from hashlib import sha512
from Crypto.Cipher import AES
PW = 'Backward, go backward, turn back to the antemundane realm, go back to the -'
d = sha512(PW.encode()).digest()
ct = open('584281739b355980d23720afec155ffc.bundle','rb').read() # c9s.test
pt = AES.new(d[:32], AES.MODE_CBC, ct[:16]).decrypt(ct[16:])
print(pt[:8]) # b'UnityFS'
Terminal window
llvm-objdump -d --arch-name=arm64 --start-address=0x1D26B70 --stop-address=0x1D26FC8 apk_extract/libil2cpp.so

::::

14. 附:bl 反查与调用图#

# c9_callgraph.py 核心四步:
# 1) dump.cs 中 "// RVA: 0x…" + 签名 → 方法表(含所属类),排序后 bisect 求「地址属于哪个方法」
# 2) ELF program header 取可执行段 (p_vaddr, p_offset, p_filesz) → vaddr↔file offset
# 3) 逐方法扫 4 字节对齐的 BL:(w & 0xFC000000) == 0x94000000
# target = a + (sign_extend(w & 0x03FFFFFF) << 2)
# 4) target 与调用点都映射回「类::方法」→ 1624 条边的调用图

15. 边界与存疑#

  • CanAdvance / AreKeyCollectionsComplete / get_PasswordIsCorrect 无直接 bl(泛型或委托),语义来自签名与调用半径。
  • LifeTiers 的具体数值在静态数组初值(RuntimeHelpers.InitializeArray)里;策略函数(DeductsLifeOnGood / PromoteAfterFailure / ApplyAfterSuccess)已确认。
  • LoadScene 的参数字符串(场景名)未能从字面量一一对回去,本文只标「谁在切场景」。
  • 8 个 c9s.* 里「首 16 字节是否 IV」不一致,按 §8.1 双策略处理。

剧情向:主线全章总结与解密出的终章原文,见《穹顶孤舟:主线的完整总结》。 素材向:视频 / 试听 / 曲绘 / 材质 / 截图全量归档,见《素材馆》。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
Phigros 4.0.1 第九章客户端全解:从启动到退出,这一章在程序里究竟发生了什么
https://blog.wmc.pub/posts/phi9-chapter9-client-internals/
作者
Milk
发布于
2026-10-04
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
Phigros 第九章《穹顶孤舟》网页解谜复盘:从 Chaocipher 到 13 段碎片拼字
技术一条完整可复现的解谜链:Chaocipher 解出隐藏页、PNG 尾部藏图、零宽字符转摩尔斯、Nihilist 解出 JUSTENGAGEWTH、按浏览器指纹分发的 13 段密文,以及 acrostic 竖读出的两个关键词。
2
Phigros 第九章 ARG 收官:17:00 归零那一刻,与藏在控制台里的致谢
技术Solivault 的倒计时在 2026-10-02 17:00 归零。终点页在归零后两分钟内换了一版:源码从混淆变回明文、版本号从 2.2.0 跳到 2.4.0,并多出一段只在浏览器控制台里打印的制作人员名单。这篇记录我抓到的完整时间窗和那几个藏起来的机制。
3
穹顶孤舟:Phigros 主线全章总结(含从秘密包里解出的终章)
随笔主线已经收束。这篇把一~九章连起来讲一遍:Legacy 里的一~四章、五《方舟蜃景》到八《弭刻日》的门槛与关底、第九章《穹顶孤舟》的 12 首曲与解密档案,以及最后从客户端秘密包里解出来的 C9S6 终章对白。
4
穹顶孤舟 · 素材馆:把第九章整个装进一页
随笔第九章的全部可听可见之物都在这:选难度的六路视频、12 首曲的官方试听与 12 张曲绘、隐藏曲 PV、结局循环、秘密曲、31 段音效切片、10 张材质(噪域/故障/网格/密码界面)、8 组界面截图,以及完整清单表与出处。
5
Solivault 的歌里藏了什么:一次 SSTV 解码实录
技术Solivault 页面改成会播歌之后,我们把音频从 WebCrypto 解密链路里捞出来,发现节拍里藏着一条 SSTV 传输——解出一张 320×256 的角色立绘。
随机文章随机推荐

评论区

Profile Image of the Author
Milk
AWMC Founder
分类
标签
站点统计
文章
10
分类
3
标签
22
总字数
58,773
运行时长
0 天
最后活动
0 天前
1
1. 数据地图
2
2. 全流程:从启动到退出
2.1 启动:先把解锁与密钥恢复回来
2.2 进章:Phase 1(选曲)
2.3 难度动画:选难度是「六路视频 + 定格 + 畸变」
2.4 打关:进度不在关卡里推进,而在结算里推进
2.5 Phase 2:换形态,并进入谜题 / 推演
2.6 谜题与推演
2.7 密码:一次输入,换来整包秘密资产
2.8 收束:S6 → Hate → DS → Message → HappyEnding → Credits
2.9 退出:程序在哪里主动退出(共 5 处)
2.10 场景名:确证的四个 + 共用槽位
3
3. 第九章的全部条件
3.1 章节解锁
3.2 12 首曲 = 12 个键
3.3 两台状态机
4
4. 逻辑与判定:状态、条件、触发
4.1 会话核心 Chapter9Phase2Session(TypeDefIndex 4404)
4.2 判定纯函数 Chapter9Phase2Rules
4.3 每首曲的附加条件(unlockInfo 数据原文)
4.4 LevelMod 清单(global-metadata 字面量)
4.5 噪域的触发条件(结论来自代码)
4.6 档案(收藏品):44 条与它们的分类
5
5. 新元素:噪域(Noise Field)
5.1 噪域会不会「取消判定」?(把边界说清)
6
6. 选曲界面怎么换成第二形态
7
7. LIFE 挑战(秘密挑战)
8
8. 课题界面:那些「乱码」是设计好的
9
9. 密码与秘密包:算法、加载、卸载
9.1 判定链
9.2 秘密包的运行时生命周期(Addressables 侧)
9.3 8 个包里有什么
10
10. 声音:这一章的“声音”是在什么时候、以什么方式变的
10.1 骨架
10.2 三个“一等例子”
10.3 音频的容器与参数(怎么存的)
10.4 启动 BGM 会因为你「有没有通关第九章」而不同
10.5 音效一览(挑有代表性的)
10.5 两处纠正(自我否定)
11
11. 用剧情解读程序:每个子系统对应哪一幕
12
12. 谱面与素材
13
13. 复现命令
14
14. 附:bl 反查与调用图
15
15. 边界与存疑
文章目录
1
1. 数据地图
2
2. 全流程:从启动到退出
2.1 启动:先把解锁与密钥恢复回来
2.2 进章:Phase 1(选曲)
2.3 难度动画:选难度是「六路视频 + 定格 + 畸变」
2.4 打关:进度不在关卡里推进,而在结算里推进
2.5 Phase 2:换形态,并进入谜题 / 推演
2.6 谜题与推演
2.7 密码:一次输入,换来整包秘密资产
2.8 收束:S6 → Hate → DS → Message → HappyEnding → Credits
2.9 退出:程序在哪里主动退出(共 5 处)
2.10 场景名:确证的四个 + 共用槽位
3
3. 第九章的全部条件
3.1 章节解锁
3.2 12 首曲 = 12 个键
3.3 两台状态机
4
4. 逻辑与判定:状态、条件、触发
4.1 会话核心 Chapter9Phase2Session(TypeDefIndex 4404)
4.2 判定纯函数 Chapter9Phase2Rules
4.3 每首曲的附加条件(unlockInfo 数据原文)
4.4 LevelMod 清单(global-metadata 字面量)
4.5 噪域的触发条件(结论来自代码)
4.6 档案(收藏品):44 条与它们的分类
5
5. 新元素:噪域(Noise Field)
5.1 噪域会不会「取消判定」?(把边界说清)
6
6. 选曲界面怎么换成第二形态
7
7. LIFE 挑战(秘密挑战)
8
8. 课题界面:那些「乱码」是设计好的
9
9. 密码与秘密包:算法、加载、卸载
9.1 判定链
9.2 秘密包的运行时生命周期(Addressables 侧)
9.3 8 个包里有什么
10
10. 声音:这一章的“声音”是在什么时候、以什么方式变的
10.1 骨架
10.2 三个“一等例子”
10.3 音频的容器与参数(怎么存的)
10.4 启动 BGM 会因为你「有没有通关第九章」而不同
10.5 音效一览(挑有代表性的)
10.5 两处纠正(自我否定)
11
11. 用剧情解读程序:每个子系统对应哪一幕
12
12. 谱面与素材
13
13. 复现命令
14
14. 附:bl 反查与调用图
15
15. 边界与存疑