data: switch to recovered game DB and refresh cards and charts
Publish GitHub Pages output / publish (push) Successful in 44s

This commit is contained in:
2026-09-29 22:48:08 +09:00
parent 5783072b12
commit 07b98db520
1259 changed files with 11228 additions and 6809 deletions
+38
View File
@@ -0,0 +1,38 @@
# 게임 데이터 원본
2026-09-29부터 마스터 데이터의 유일한 수집 원본은 직접 확보한 한국어 Android 게임 DB입니다.
현재 버전은 `546b8e2e37db368c6731e148b88611cef105bbaa66ff567f5c1584711ef979a4`이며,
379개 SQLite 파일, 233,312행의 무결성 및 protobuf 해석을 검증했습니다.
`data/game-db/current.json`이 버전별 `snapshots/<version>/manifest.json`을 SHA-256으로 고정합니다.
스냅샷에는 전체 테이블의 압축 JSON과 DB/JSON 해시만 저장하며, 복호화 키·메모리 덤프·계정 인증은 포함하지 않습니다.
`data/holodoridb/`와 과거 연구 기록은 이전 수집의 기록으로만 남으며 현재 수집기가 읽지 않습니다.
## 갱신
1. 게임의 새 DB를 확보·복호화합니다. 복구 디렉터리에 `decrypted-master/`, `decrypt-manifest.json`, 해당 앱 버전의 `descriptors.json`이 필요합니다.
2. protobuf가 설치된 Python으로 `python scripts/import-game-db.py <복구 디렉터리>`를 실행합니다. SQLite 해시·무결성·알 수 없는 protobuf 필드를 검사한 뒤 전체 스냅샷을 등록합니다. 같은 버전의 내용 교체는 거부합니다.
3. `npm run data:sync`로 전체 테이블을 마지막 적용 스냅샷과 비교합니다. `data/game-db/review/latest.json`의 전체 변경 전후 값과 `latest.md`를 검토하고 한국어로 변경 의미를 보고합니다. 첫 등록은 전체 스냅샷 등록으로 표시하며 모든 행을 신규 콘텐츠로 간주하지 않습니다.
4. `npm run data:apply`로 카드·보드·메모리·홀로워크 업적을 생성합니다. 검토와 같은 스냅샷만 허용하며 성공 후에만 `applied.json`을 갱신합니다.
5. 채보를 갱신할 때는 별도 게임 SUS 파일을 `data/raw/sus/`에 확보합니다. `npm run data:download-charts`는 공식 게임 CDN에 직접 접속합니다. `npm run data:collect-charts`는 로컬 SUS와 DB를 검증한 뒤 공개 시각이 지난 곡만 생성합니다. `-- --existing`은 기존 카탈로그만 재생성합니다. 이어 `npm run data:collect-chart-visuals`로 동일 SUS의 표시 데이터를 만듭니다.
수집 실패·누락·해시 불일치 시 다른 사이트나 예전 DB로 대체하지 않습니다. 지금 가진 스냅샷은 캡처 당시 버전이며, 게임의 다음 업데이트는 새로 확보해야 합니다.
실행 중인 게임에서 DB를 복구하는 과정은 아직 별도 작업입니다. 스냅샷 등록 명령이 자동으로 게임 업데이트를 감지하거나 DB를 복호화하지 않습니다.
## DB만으로 대체할 수 없는 자료
| 자료 | 현재 원본과 처리 |
| --- | --- |
| 카드·스킬·보드·메모리·업적·한국어 문자열·악곡 메타데이터 | 현재 게임 DB. 영어·일본어 외부 번역을 가져오지 않음 |
| 채보 본문·레인·노트 시각·점수 분모 | 게임 SUS 파일. DB의 난이도·노트 수·계수와 교차 검증. 외부 계산기 채보나 점수 참고표를 생성 입력으로 사용하지 않음 |
| 과거 이벤트 기간·배율 | `src/lib/data/events.ts`의 기존 수동 기록. 당시 외부 출처 주석을 유지. 현재 DB에서 이벤트별 설정을 찾지 못함. 앱의 EventListEventInfoResponse에 일정·특효 필드가 존재하나 실제 응답은 아직 미수집 |
| 카드 별칭·표시명 | `data/card-overrides.csv`의 사용자 편집 설정 |
이번 전환에서 기존 200곡/800채보를 로컬 게임 파일로 다시 생성하고, 공식 게임 CDN의 Octo revision 93에서 신규·누락 9곡의 SUS 36개를 확보했습니다. 현재 공개된 209곡/836채보를 생성했습니다. 아직 공개되지 않은 2곡은 DB 스냅샷에만 보관합니다.
공식 게임 자산과 서버의 이벤트 설정까지 DB 하나에 들어 있는 것은 아닙니다. HolodoriDB 원격 저장소나 다른 계산기 사이트 접속은 기본 수집 과정에서 필요하지 않습니다.
첫 전환의 기존 데이터 비교는 [DB 비교 기록](research/android-master-comparison-2026-09-29.md)을 참고합니다.
기존 외부 점수 참고표와 달랐던 m0337(Pretty Guardian Matsulimit Zero)의 분모는 공식 게임 CDN revision 93에서 재확인했습니다. Hard는 544300 → 539500, Expert는 874750 → 884700이며, 같은 노트 수라도 비가시·제어점 가중치가 포함되는 분모는 별도로 검증해야 합니다. 검증용 게임 SUS는 scripts/fixtures/game-sus/에 보관합니다.
이벤트 설정의 DB·CDN 조사 범위와 서버 API 스키마 확인 결과는 [이벤트 설정 원본 조사](research/event-config-source-2026-09-29.md)를 참고합니다. CDN 목록은 조사했지만 모든 번들 내부를 검사한 것은 아닙니다.
@@ -0,0 +1,293 @@
# 계산기 데이터와 Android 복호화 DB 비교 — 2026-09-29
## 결론
기존 카드 185개의 계산용 데이터는 일치한다. DB에는 신규 카드 4개, 곡 7개, 포스터 1개, 보드 노드 1개가 추가되어 있어 전체 데이터는 일치하지 않는다. 기존 카드 성장치와 스킬 수치가 바뀐 것은 아니다.
비교 대상은 현재 작업 폴더의 계산기 데이터다. 배포된 사이트의 파일은 이번 조사에서 별도 확인하지 않았다. 앱 데이터 수정·배포는 하지 않았다.
## 비교 기준과 검증
- 계산기 마스터 버전: `845cee36af22b657511ca406b5417cac7076a001208518c9601d6389a5046ddd`
- 복호화 DB 마스터 버전: `546b8e2e37db368c6731e148b88611cef105bbaa66ff567f5c1584711ef979a4`
- 공식 eng/kor/jpn 원격 main을 `npm run data:sync`로 fetch했다. 모두 기존 9월 26일 수집 기준과 동일했고 카드/스킬·곡·이벤트·번역·기타 시스템의 원격 추가 변경은 없었다. 세 검토 파일을 모두 확인했다.
- 현재 Android 1.2.0 바이너리에서 Protocol Buffers 파일 정의 612개를 추출했다. 실제 DB 379개, 233,312행의 레코드를 모두 해석했고 오류나 해석되지 않은 protobuf 필드는 없었다.
- enum 접두사와 기본값 표기를 정규화했다. DB의 보조 검색 컬럼 추가를 레코드 삭제/추가로 오인하지 않도록 기존 식별자로 비교했다. 큰 정수 client_id는 원본 JSON의 정밀도 손실을 피하도록 protobuf의 정확한 문자열 ID를 사용했다.
- 현재 수집 코드를 조사 폴더의 격리된 출력으로 실행해 기준 원격과 DB의 계산기 요약을 각각 재생성했다. 실제 서비스 출력 경로는 변경하지 않았다.
- 현재 card-master의 34개 표 및 저장된 번역은 기준 원격과 일치한다. 카드·보드·메모리 요약 3개 파일도 기준 원격을 재생성한 결과와 정확히 일치한다.
- 에뮬레이터에서 확보한 번역은 한국어다. 신규 영어·일본어 번역의 최신성은 검증하지 않았다. 비교용 생성에서 이 두 언어는 기준 원격 자료를 유지했으며 한국어 화면/계산용 요약만 최신 DB와 검증했다.
## 카드와 스킬
기존 185개의 카드 원본 레코드 및 정규화된 계산기 카드 객체에 변경·삭제가 없다. 최대 레벨 능력치, 패시브/액티브/스페셜, 연결 효과, 의상 설명 등 계산기에 저장되는 기존 카드의 전체 필드가 동일하다.
| 신규 카드 | 속성 | 한국어 카드명 | ID |
|---|---|---|---|
| 호쇼 마린 | 퓨어 | 보물 독점♡워터 캐논 | `card-00023-5-uniq-0085-00` |
| 하쿠이 코요리 | 해피 | 뒤돌아본 순간의 aventure | `card-00037-5-uniq-0086-00` |
| 코보 카나에루 | 큐트 | 파도 사이에 흔들리는 Body & Soul | `card-03009-5-uniq-0088-00` |
| 하코스 벨즈 | 해피 | 이게 나의 강추! | `card-04012-5-uniq-0087-00` |
신규 카드는 모두 ★5다. 관련 패시브/액티브/스페셜 정의는 각각 4개, 각 스킬 레벨 표는 각각 8행 추가됐다. 액티브 효과 2개가 추가됐고 기존 효과 수치는 동일하다. 의상과 SD 의상은 각각 7개 추가됐으며 의상 리더 스킬도 7개 추가됐다. 카드 레벨·한계돌파·잠재력 성장 표와 스킬 발동 조건 표는 동일하다.
## 곡과 채보
기준 마스터 204곡 → 복호화 DB 211곡. 난이도 표는 816행 → 844행이다. 현재 계산기 정확 채보 카탈로그는 200곡·800개 난이도다. 포함된 800개 모두 곡명, 난이도 레벨, 시작 시간, 가창 유형/멤버가 DB와 같고, 저장된 점수 공식의 곡 계수도 같다.
| 신규 곡 | ID | DB 시작 시간 (KST) | EASY / NORMAL / HARD / EXPERT |
|---|---|---|---|
| PARADISE! | `m0354` | 2026-09-29 11:00 | 4 / 10 / 19 / 26 |
| Play Dice! | `m0357` | 2026-09-29 12:00 | 4 / 11 / 20 / 28 |
| PROPOSE | `m0358` | 2026-09-29 12:00 | 3 / 10 / 18 / 25 |
| BAKU LOVE CHEMISTRY | `m0413` | 2026-09-29 12:00 | 5 / 13 / 20 / 28 |
| Kyapi | `m0548` | 2026-09-29 12:00 | 4 / 10 / 17 / 26 |
| Wonky Monkey | `m0375` | 2026-10-01 00:00 | 5 / 11 / 18 / 25 |
| KONKON Beats | `m0376` | 2026-10-05 00:00 | 5 / 12 / 18 / 26 |
10월 1일과 5일의 값은 DB에 선행 포함된 시작 시간이며, 실제 서비스 일정 확정이나 현재 플레이 가능을 뜻하지 않는다. 곡명은 확보한 Music.titleLangId → LangMusic_Kor 관계로 확인했다.
이번 신규 7곡 외에, 기준 마스터에 이미 있었지만 현재 계산기 정확 채보 카탈로그에 없는 4곡도 있다:
- **Glitch Through** (`m0351`), DB 시작 2026-09-21 20:00 KST
- **64.** (`m0352`), DB 시작 2026-09-25 20:00 KST
- **Watcha Gatcha!!!!!!!!** (`m0525`), DB 시작 2026-09-26 21:00 KST
- **Lethal Dose Paranoid** (`m0350`), DB 시작 2026-09-23 20:00 KST
기존 4곡(m0349~m0352)에는 MV URL이 추가됐다. 기존 곡의 다른 Music 필드와 난이도 레벨은 동일하다. MusicDifficultyChart의 기존 816행에서는 이전 스키마의 `chartHash`, `normalNoteCount`가 사라졌고 다른 기존 필드는 동일하다. 이를 816개 채보 자체의 변경으로 판정하지 않았다.
실제 노트 시각·롱노트 형상·노트별 점수 가중치 등 SUS 채보 본문은 이 마스터 DB에 없으므로, 현재 채보 파일 자체의 동일성은 이번 비교에서 검증하지 않았다.
## 보드·메모리·업적
- 기존 보드 노드의 계산 수치와 조건은 동일하다. 4개 보드 모델에 `G-025`가 각 1개씩 추가되어 모델별 153 → 154개 위치가 된다.
- 신규 G-025는 스플래시 볼 보상량 +5‰(0.5%) 효과다. 플레이어 레벨 30 조건, 포인트 3, 공용 보드 아이템 160개 비용이 포함돼 있다.
- 기존 Y-004, Y-018, Y-022 설명이 **“전체 악곡” → “단체 악곡”**으로 수정됐다. 효과 수치/대상 조건 자체가 바뀐 것은 아니다.
- 포스터 34 → 35개. `poster-marathon_event-normal-005-chapter-01`이 추가됐다. 수집 보너스 표는 그대로이며 보유 상한에서 적용 가능한 값이 64‰ → 65‰(6.4% → 6.5%)가 된다.
- 홀로워크 업적의 멤버 54명별 완료 횟수 임계값은 현재 계산기와 전부 일치한다.
## 이벤트·번역·기타 시스템
- EventStory 30행, EventStoryChapter 11행은 기존과 동일하다. 신규 이벤트 관련 포스터 참조는 있지만, 이 DB에는 MarathonEvent 일정·보너스 표가 없어 현재 events.ts와 최신 이벤트 일정/배율의 일치를 확정할 수 없다.
- 한국어 문자열은 확보된 123개 Lang 표에서 4,685행 추가, 9행 수정, 1행 삭제다. 보드 표현 수정 외에 소다팝 라군 게이트 이름/설명, 대사 오역 수정, 이벤트 보너스 안내 수정이 있다.
- 도움말에는 “스킬 발동 빈도 UP” 해방 시 편성에 따라 유닛 스코어 표시값이 낮아질 수 있다는 설명이 추가됐다. 이는 설명 변경으로 확인했으며 계산 공식 변경의 증거로 취급하지 않았다.
- 캐릭터 62명의 color5가 추가됐다. 소다팝 라군 관련 지역/시설, 파크 NPC·대사·위치·퀘스트, 낚시 도감/추첨/시간대, 스플래시 볼 보상 등 시스템 데이터가 추가됐다.
- 새 DB 표 8개: `LangParkTimeTransition_Kor`, `MusicStartupNotification`, `ParkFishingFishBook`, `ParkFishingFishLottery`, `ParkFishingTimePeriod`, `ParkTimeTransition`, `SplashBallScoreReward`, `SplashBallTeamRankReward`.
- 기준에는 있었던 LangChasePatrolSkill_Kor(4행), LangParkHighLow_Kor(1행)는 이번 DB 묶음에 없다. 관련 필드가 새 스키마에서 사라진 점은 확인했으나, 게임 기능 자체가 삭제되었다고 단정하지 않는다.
- 여러 기타 시스템의 필드 제거·추가도 포함된다. 이름만으로 의미를 확정하기 어려운 변경은 아래 표 및 전체 필드 차이 JSON에 원문 값으로 보존했다.
## 전체 변경 집계
현재 DB 379개 중 175개는 기준과 레코드 내용이 같고, 204개는 신규 표 또는 레코드 차이가 있다. 현재 DB에 존재하는 표끼리 비교하면 45,045행 추가, 1행 삭제, 1,522행 수정이다. 별도로 기준 번역 표 2개(총 5행)가 이번 DB 묶음에 없다. 큰 변경 수에는 신규 파크 NPC·대사 등과 스키마 필드 변경이 포함되며, 계산기 수치가 그만큼 바뀌었다는 뜻은 아니다.
전체 전후 필드 값: `data/inspection/android-master-2026-09-29/db-comparison.json`
실제 계산기 데이터 비교: `data/inspection/android-master-2026-09-29/app-comparison.json`
해석된 DB JSON: `data/inspection/android-master-2026-09-29/decoded-json/`
| 표 | 이전 행 | DB 행 | 추가 | 삭제 | 수정 |
|---|---:|---:|---:|---:|---:|
| AchievementGroup | 77 | 79 | 2 | 0 | 0 |
| AppealText | 16 | 17 | 1 | 0 | 0 |
| Area | 8 | 9 | 1 | 0 | 0 |
| AreaAppearance | 22 | 30 | 8 | 0 | 0 |
| AreaGrouping | 2 | 3 | 1 | 0 | 0 |
| AreaMap | 14 | 22 | 8 | 0 | 0 |
| AreaSelector | 7 | 8 | 1 | 0 | 1 |
| AreaStory | 17 | 23 | 6 | 0 | 0 |
| AreaStoryChapter | 2 | 3 | 1 | 0 | 0 |
| AssetEmbeddingText | 62 | 87 | 25 | 0 | 0 |
| AssetStreamingMute | 72 | 73 | 1 | 0 | 0 |
| Card | 185 | 189 | 4 | 0 | 0 |
| Character | 62 | 62 | 0 | 0 | 62 |
| CharacterLiveDialogueVoice | 6336 | 6432 | 96 | 0 | 0 |
| CharacterVoice | 16582 | 18187 | 1605 | 0 | 0 |
| ChasePatrolSkill | 3 | 3 | 0 | 0 | 3 |
| ChaseSetting | 1 | 1 | 0 | 0 | 1 |
| Cheat | 60 | 77 | 17 | 0 | 10 |
| CircuitSetting | 1 | 1 | 0 | 0 | 1 |
| Condition | 2267 | 2768 | 501 | 0 | 1 |
| Costume | 193 | 200 | 7 | 0 | 0 |
| Emblem | 949 | 987 | 38 | 0 | 39 |
| ExchangeBooth | 294 | 304 | 10 | 0 | 0 |
| ExchangeBoothFixedItem | 1044 | 1306 | 262 | 0 | 0 |
| ExchangeBoothGroup | 70 | 76 | 6 | 0 | 0 |
| Facility | 65 | 108 | 43 | 0 | 0 |
| FacilityAppearance | 187 | 271 | 84 | 0 | 2 |
| FacilityLevel | 30 | 51 | 21 | 0 | 0 |
| FeatureLock | 47 | 53 | 6 | 0 | 0 |
| Gacha | 17 | 21 | 4 | 0 | 0 |
| GachaButton | 38 | 46 | 8 | 0 | 0 |
| GachaPoint | 8 | 10 | 2 | 0 | 0 |
| HelpContent | 127 | 131 | 4 | 0 | 0 |
| InstantTips | 16 | 22 | 6 | 0 | 0 |
| Item | 126 | 133 | 7 | 0 | 0 |
| JumpRopePhaseGimmick | 69 | 69 | 0 | 0 | 43 |
| JumpRopeSetting | 1 | 1 | 0 | 0 | 1 |
| LangAchievementGroup_Kor | 77 | 79 | 2 | 0 | 0 |
| LangAppealText_Kor | 32 | 34 | 2 | 0 | 0 |
| LangArea_Kor | 16 | 18 | 2 | 0 | 0 |
| LangAreaGrouping_Kor | 4 | 6 | 2 | 0 | 0 |
| LangAreaStory_Kor | 17 | 23 | 6 | 0 | 0 |
| LangAreaStoryChapter_Kor | 2 | 3 | 1 | 0 | 0 |
| LangAssetEmbeddingText_Kor | 62 | 87 | 25 | 0 | 0 |
| LangCard_Kor | 185 | 189 | 4 | 0 | 0 |
| LangCharacterVoice_Kor | 26907 | 28813 | 1906 | 0 | 0 |
| LangClientCommon_Kor | 3196 | 3338 | 143 | 1 | 0 |
| LangCondition_Kor | 82 | 95 | 13 | 0 | 0 |
| LangCostume_Kor | 193 | 200 | 7 | 0 | 0 |
| LangEmblem_Kor | 949 | 987 | 38 | 0 | 0 |
| LangFacility_Kor | 107 | 144 | 37 | 0 | 2 |
| LangFacilityAppearance_Kor | 28 | 41 | 13 | 0 | 0 |
| LangFacilityLevel_Kor | 21 | 36 | 15 | 0 | 0 |
| LangFeatureLock_Kor | 54 | 60 | 6 | 0 | 0 |
| LangGachaPoint_Kor | 8 | 10 | 2 | 0 | 0 |
| LangGeneratedLiveActiveSkillEffect_Kor | 53 | 55 | 2 | 0 | 0 |
| LangGeneratedLiveActiveSkillLevel_Kor | 373 | 381 | 8 | 0 | 0 |
| LangGeneratedLiveLeaderSkill_Kor | 185 | 192 | 7 | 0 | 0 |
| LangGeneratedLivePassiveSkillLevel_Kor | 370 | 378 | 8 | 0 | 0 |
| LangGeneratedLiveSpecialSkillLevel_Kor | 370 | 378 | 8 | 0 | 0 |
| LangGeneratedSkillTreeEffect_Kor | 121 | 122 | 1 | 0 | 2 |
| LangGeneratedSkillTreeEffectPassiveTrigger_Kor | 81 | 81 | 0 | 0 | 1 |
| LangHelpContent_Kor | 256 | 264 | 8 | 0 | 1 |
| LangInstantTips_Kor | 11 | 15 | 4 | 0 | 0 |
| LangItem_Kor | 252 | 266 | 14 | 0 | 0 |
| LangLiveDialogueVoice_Kor | 3353 | 3405 | 52 | 0 | 0 |
| LangLiveSkin_Kor | 87 | 89 | 2 | 0 | 0 |
| LangLiveSkinGroup_Kor | 56 | 57 | 1 | 0 | 0 |
| LangLoadingDisplay_Kor | 104 | 110 | 6 | 0 | 0 |
| LangMainStory_Kor | 36 | 51 | 15 | 0 | 0 |
| LangMainStoryChapter_Kor | 4 | 6 | 2 | 0 | 0 |
| LangMiniGame_Kor | 20 | 24 | 4 | 0 | 0 |
| LangMission_Kor | 1666 | 1794 | 128 | 0 | 0 |
| LangMissionGroup_Kor | 24 | 26 | 2 | 0 | 0 |
| LangMusic_Kor | 2096 | 2167 | 71 | 0 | 0 |
| LangMusicMv_Kor | 154 | 157 | 3 | 0 | 0 |
| LangParkAccessory_Kor | 163 | 217 | 54 | 0 | 0 |
| LangParkEmotion_Kor | 918 | 1188 | 270 | 0 | 0 |
| LangParkFishingBait_Kor | 0 | 16 | 16 | 0 | 0 |
| LangParkFishingFish_Kor | 0 | 162 | 162 | 0 | 0 |
| LangParkFishingFishSpecies_Kor | 0 | 4 | 4 | 0 | 0 |
| LangParkFishingRod_Kor | 0 | 8 | 8 | 0 | 0 |
| LangParkPermanenceAction_Kor | 3 | 9 | 6 | 0 | 0 |
| LangParkQuest_Kor | 272 | 340 | 68 | 0 | 0 |
| LangParkQuestChapter_Kor | 59 | 61 | 2 | 0 | 0 |
| LangParkQuestStep_Kor | 768 | 923 | 155 | 0 | 0 |
| LangParkTalk_Kor | 4432 | 5577 | 1145 | 0 | 1 |
| LangParkTargetMapping_Kor | 26 | 48 | 22 | 0 | 0 |
| LangParkTimeTransition_Kor | 0 | 12 | 12 | 0 | 0 |
| LangPoster_Kor | 34 | 35 | 1 | 0 | 0 |
| LangResourceObtainmentRoute_Kor | 42 | 50 | 8 | 0 | 0 |
| LangResourceTypeDetail_Kor | 49 | 50 | 1 | 0 | 0 |
| LangSdCostume_Kor | 301 | 308 | 7 | 0 | 0 |
| LangSdCostumeHairAccessory_Kor | 270 | 378 | 108 | 0 | 0 |
| LangSpecialStory_Kor | 2 | 3 | 1 | 0 | 0 |
| LangSplashBall_Kor | 0 | 4 | 4 | 0 | 0 |
| LangTips_Kor | 136 | 162 | 26 | 0 | 0 |
| LangTipsDetail_Kor | 281 | 315 | 34 | 0 | 2 |
| LangWork_Kor | 4 | 5 | 1 | 0 | 0 |
| LiveActiveSkill | 187 | 191 | 4 | 0 | 0 |
| LiveActiveSkillEffect | 53 | 55 | 2 | 0 | 0 |
| LiveActiveSkillLevel | 373 | 381 | 8 | 0 | 0 |
| LiveDialogueVoice | 3353 | 3405 | 52 | 0 | 0 |
| LiveDialogueVoiceCharacter | 6521 | 6621 | 100 | 0 | 0 |
| LiveLeaderSkill | 185 | 192 | 7 | 0 | 0 |
| LivePassiveSkill | 185 | 189 | 4 | 0 | 0 |
| LivePassiveSkillLevel | 370 | 378 | 8 | 0 | 0 |
| LiveRewardUpStaminaEffect | 10 | 10 | 0 | 0 | 3 |
| LiveSkin | 282 | 283 | 1 | 0 | 0 |
| LiveSkinGroup | 56 | 57 | 1 | 0 | 0 |
| LiveSpecialCardDialogue | 307 | 335 | 28 | 0 | 0 |
| LiveSpecialCostumeDialogue | 307 | 335 | 28 | 0 | 0 |
| LiveSpecialSkill | 185 | 189 | 4 | 0 | 0 |
| LiveSpecialSkillLevel | 370 | 378 | 8 | 0 | 0 |
| LoadingDisplay | 107 | 110 | 3 | 0 | 0 |
| MainStory | 36 | 51 | 15 | 0 | 0 |
| MainStoryChapter | 2 | 3 | 1 | 0 | 0 |
| MarathonMiniGameRewardUpStaminaEffect | 12 | 15 | 3 | 0 | 0 |
| MiniGame | 5 | 6 | 1 | 0 | 0 |
| MiniGameMultiGame | 8 | 10 | 2 | 0 | 0 |
| MiniGameNpcLotteryPeriod | 4 | 5 | 1 | 0 | 0 |
| MiniGameRewardUpStaminaConversion | 1 | 2 | 1 | 0 | 0 |
| MiniGameRewardUpStaminaEffect | 12 | 15 | 3 | 0 | 0 |
| Mission | 1651 | 1745 | 94 | 0 | 0 |
| MissionGroup | 27 | 30 | 3 | 0 | 0 |
| MissionPass | 3 | 3 | 0 | 0 | 1 |
| MissionPassLevel | 153 | 153 | 0 | 0 | 1 |
| MissionProgress | 11420 | 11625 | 205 | 0 | 0 |
| MissionTypeDefaultNavigation | 107 | 113 | 6 | 0 | 2 |
| Music | 204 | 211 | 7 | 0 | 4 |
| MusicDifficulty | 816 | 844 | 28 | 0 | 0 |
| MusicDifficultyChart | 816 | 844 | 28 | 0 | 816 |
| MusicMv | 288 | 294 | 6 | 0 | 0 |
| MusicStartupNotification | 0 | 64 | 64 | 0 | 0 |
| OctoSetting | 1 | 1 | 0 | 0 | 1 |
| ParkAccessory | 190 | 244 | 54 | 0 | 0 |
| ParkCall | 64 | 71 | 7 | 0 | 0 |
| ParkCharacterShop | 4 | 5 | 1 | 0 | 0 |
| ParkEmotion | 918 | 1188 | 270 | 0 | 0 |
| ParkFishingBait | 0 | 8 | 8 | 0 | 0 |
| ParkFishingFish | 0 | 81 | 81 | 0 | 0 |
| ParkFishingFishBook | 0 | 3 | 3 | 0 | 0 |
| ParkFishingFishIcon | 0 | 10 | 10 | 0 | 0 |
| ParkFishingFishLottery | 0 | 2925 | 2925 | 0 | 0 |
| ParkFishingFishSpecies | 0 | 4 | 4 | 0 | 0 |
| ParkFishingRod | 0 | 4 | 4 | 0 | 0 |
| ParkFishingSetting | 1 | 1 | 0 | 0 | 1 |
| ParkFishingSpot | 0 | 12 | 12 | 0 | 0 |
| ParkFishingTimePeriod | 0 | 2 | 2 | 0 | 0 |
| ParkFishingTotalCaughtReward | 0 | 18 | 18 | 0 | 0 |
| ParkFixedSymbol | 2 | 108 | 106 | 0 | 0 |
| ParkGraph | 315 | 368 | 53 | 0 | 0 |
| ParkHighLow | 1 | 1 | 0 | 0 | 1 |
| ParkNotice | 17 | 21 | 4 | 0 | 0 |
| ParkNpc | 13165 | 21317 | 8152 | 0 | 0 |
| ParkNpcGroup | 12728 | 20843 | 8115 | 0 | 0 |
| ParkPermanence | 115 | 149 | 34 | 0 | 0 |
| ParkPermanenceAction | 120 | 162 | 42 | 0 | 0 |
| ParkPosition | 802 | 1235 | 433 | 0 | 0 |
| ParkPositionGrouping | 197 | 356 | 159 | 0 | 0 |
| ParkPrintBoothComposition | 18 | 18 | 0 | 0 | 18 |
| ParkQuest | 244 | 292 | 48 | 0 | 244 |
| ParkQuestChapter | 59 | 61 | 2 | 0 | 0 |
| ParkQuestCookingPuzzle | 14 | 14 | 0 | 0 | 14 |
| ParkQuestLive | 130 | 144 | 14 | 0 | 0 |
| ParkQuestLiveMusicDifficulty | 520 | 576 | 56 | 0 | 0 |
| ParkQuestSplashBall | 0 | 2 | 2 | 0 | 0 |
| ParkQuestStep | 766 | 921 | 155 | 0 | 0 |
| ParkQuestStepAction | 774 | 929 | 155 | 0 | 0 |
| ParkRandomSymbol | 198 | 296 | 98 | 0 | 0 |
| ParkTalk | 4841 | 6056 | 1215 | 0 | 18 |
| ParkTalkFree | 7406 | 13633 | 6227 | 0 | 174 |
| ParkTalkFreeGrouping | 148 | 194 | 46 | 0 | 4 |
| ParkTargetMapping | 8549 | 15328 | 6779 | 0 | 0 |
| ParkTimeTransition | 0 | 6 | 6 | 0 | 0 |
| ParkTreasure | 42 | 70 | 28 | 0 | 42 |
| PlayerLevel | 50 | 70 | 20 | 0 | 0 |
| Poster | 34 | 35 | 1 | 0 | 0 |
| ResourceObtainmentRoute | 136 | 182 | 46 | 0 | 0 |
| ResourceTypeDetail | 25 | 26 | 1 | 0 | 0 |
| SdCostume | 301 | 308 | 7 | 0 | 0 |
| SdCostumeHairAccessory | 270 | 378 | 108 | 0 | 0 |
| Setting | 1 | 1 | 0 | 0 | 1 |
| ShopChargeItemProduct | 141 | 153 | 12 | 0 | 0 |
| SkillTreeEffect | 121 | 122 | 1 | 0 | 0 |
| SkillTreeNode | 324 | 325 | 1 | 0 | 0 |
| SkillTreeNodePosition | 612 | 616 | 4 | 0 | 0 |
| SpecialStory | 2 | 3 | 1 | 0 | 1 |
| SplashBall | 0 | 2 | 2 | 0 | 0 |
| SplashBallBall | 0 | 8 | 8 | 0 | 0 |
| SplashBallPlayerPosition | 0 | 18 | 18 | 0 | 0 |
| SplashBallScoreReward | 0 | 10 | 10 | 0 | 0 |
| SplashBallSetting | 1 | 1 | 0 | 0 | 1 |
| SplashBallTeamRankReward | 0 | 3 | 3 | 0 | 0 |
| Story | 153 | 175 | 22 | 0 | 0 |
| SystemSound | 301 | 363 | 62 | 0 | 0 |
| SystemVoice | 6156 | 6966 | 810 | 0 | 0 |
| Tips | 140 | 166 | 26 | 0 | 0 |
| TipsDetail | 288 | 329 | 41 | 0 | 2 |
| Tutorial | 58 | 67 | 9 | 0 | 0 |
| Work | 2040 | 2295 | 255 | 0 | 0 |
| WorkDisplay | 2 | 3 | 1 | 0 | 0 |
| WorkEvaluationRank | 200 | 250 | 50 | 0 | 0 |
| WorkSlot | 4 | 5 | 1 | 0 | 0 |
| LangChasePatrolSkill_Kor (DB 묶음에 없음) | 4 | — | — | — | — |
| LangParkHighLow_Kor (DB 묶음에 없음) | 1 | — | — | — | — |
@@ -0,0 +1,43 @@
# Android 마스터 DB 열기 경로 조사
후속 결과: 같은 날 메모리에서 379개 파일별 키를 복구하고 전체 DB의 복호화와 SQLite 검증에 성공했다. 아래 미확인 항목은 이 최초 정적 조사 시점 기준이며, 후속 검증은 [복구 기록](android-master-recovery-2026-09-29.md)을 참고한다.
2026-09-29, LDPlayer 14 / Android 14 / 게임 1.2.0에서 확인했다.
앱 파일과 프로세스는 수정하지 않았다. 테스트 계정 인증 정보 사용은 허용받았지만 이번 조사에서는 사용하지 않았다.
## 실행 상태에서 확인한 사실
- PID 4380의 파일 디스크립터에 외부 앱 저장소 `files/EB0A191797624DD3A48FA681D3061212/`의 후보 파일들이 열려 있었다.
- 같은 프로세스가 앱의 ARM64 `libsqlcipher.so`를 로드하고 있었다.
- 이 폴더에는 379개 파일이 있었다. 두 표본의 첫 32바이트에 일반 SQLite 헤더는 없었다.
- 시스템 SQLite도 함께 로드되어 있으므로, 라이브러리 존재만으로 모든 파일을 SQLCipher DB라고 판정하지 않았다.
## 현재 Android 바이너리에서 확인한 호출 경로
복사한 `libil2cpp.so`와 복호화한 IL2CPP 메타데이터를 연결하고 ARM64 명령을 확인했다. 아래 주소는 이 바이너리의 RVA이며 실행 시 절대 주소가 아니다.
1. `MasterManagerBase<object>.CreateCryptKey` (`0xB6283A0`)는 파일 정보 인터페이스의 슬롯 3을 호출한다. 메타데이터에서 이 슬롯은 `IMasterFileInfo.get_CryptoKey`다. 앞뒤 인자를 포함한 세 문자열 연결 결과를 UTF-8 바이트로 변환한다. 앞뒤 문자열의 실제 값은 이번 기록에서 확정하지 않았다.
2. `MasterDBConnectionPool.GetConnection` (`0xDC45458`)은 기존 연결을 찾고, 없으면 파일 경로와 키 바이트를 `CreateConnection`으로 넘긴다.
3. `CreateConnection` (`0xDC45678`)은 URI를 만들고 `SQLite3Manager.Open`에 플래그 `0x41`을 전달한다.
4. 키를 받는 `SQLite3Manager.Open` (`0xDC45A18`)은 내부 Open (`0xDC48138`)을 호출한 뒤 `sqlite3_key` (`0xDC48020`)에 연결과 키 바이트, 길이를 전달한다.
5. 내부 Open은 플래그가 있는 경우 `sqlite3_open_v2`를 호출한다. `0x41`은 SQLite의 READONLY(1)와 URI(0x40) 조합이다.
6. `CreateConnection`은 이어서 준비된 PRAGMA 목록을 `PreparedStatement.ExecuteNoQuery`로 실행한다.
메타데이터에는 `query_only = ON`, `journal_mode = OFF`, `locking_mode = EXCLUSIVE`, `automatic_index = 0`, `cache_size = 10`, `synchronous = 0` PRAGMA 문자열이 있다. 기본 `GetConnectionOpenPragmas`는 6개 항목 배열을 만든다. 이 문자열 각각의 배열 슬롯 연결은 아직 검증하지 않았다.
따라서 마스터 연결 코드는 SQLCipher에 키를 적용해 DB를 직접 읽는 구조로 확인된다. 별도의 평문 DB를 먼저 만드는 경로는 위 함수들에서 발견되지 않았다. 다른 경로의 존재를 배제한 것은 아니다.
## 미확인 사항
- 현재 각 파일에 대응하는 CryptoKey 값과 앞뒤 문자열의 실제 값.
- 실제 실행에서의 키와 암호화 옵션, 후보 파일 전체의 테이블 구성.
- 복사본을 독립적으로 열고 조회하는 검증은 아직 하지 않았다. 실제 마스터 복호화 성공을 의미하지 않는다.
## 재현 자료
원본 바이너리, 메타데이터, 분석 스크립트는 무시되는 `data/inspection/android-master-2026-09-29/`에 보관했다. `disasm.py`와 `open-path-assembly.txt`에 호출 근거가 있다. 덤퍼의 출력은 기존 도구 디렉터리 `data/inspection/master-direct-2026-09-26/dumper-bin/`에 생성되었으며 Android 1.2.0 분석 결과다.
- libil2cpp.so SHA-256: `f3c02abd02f8d0c5263e2181e712bf495fa83c80568df8c29414a61809c4fffe`
- libsqlcipher.so SHA-256: `91060e3d9360d4ac0858633f24ed97a62b66b6cdf18c00f8ef8a1e39e72fd76f`
공식 HolodoriDB main은 앞선 동일 조사 세션에서 fetch했고 3개 언어 모두 9월 26일 수집 기준과 같은 커밋이었다. 계산기 데이터 수집·반영은 실행하지 않았다.
@@ -0,0 +1,54 @@
# Android 메모리에서 마스터 키 복구 및 DB 복호화
2026-09-29, LDPlayer 14 / Android 14 / 게임 1.2.0, PID 4380에서 수행했다.
## 결과
- 게임의 마스터 관리 객체에서 파일별 경로와 키 **379개**를 복구했다.
- 원본 암호화 파일 **379개, 61,485,056바이트**를 PC로 복사했다.
- **15,011개 페이지 전부**의 HMAC-SHA512를 검증하고 AES-256-CBC 복호화에 성공했다.
- 복호화된 **379개 DB 전부**를 독립적인 Python 표준 SQLite로 읽기 전용으로 열었다. 모든 `PRAGMA integrity_check` 결과가 `ok`였으며, 모든 테이블을 끝까지 읽어 **233,312행**을 확인했다.
- Card는 189행, Music은 211행, LangMusic_Kor의 Lang 테이블은 2,167행이다. 행 내부에는 BLOB이 포함되므로 계산기용 필드/JSON 변환을 완료했다는 뜻은 아니다.
- 게임 프로세스/파일의 수정, 후킹, 재시작, 서버 요청, 계정 인증 정보 사용은 하지 않았다.
- 계산기 서비스 데이터에는 반영하지 않았다.
## 확보 방법
1. root ADB에서 `/proc/4380/maps`를 읽고 `/proc/4380/mem`의 필요한 영역을 덤프했다. LDPlayer `su` 경유 바이너리 출력의 줄바꿈 변환을 피하기 위해 기기 쪽에서 base64로 감싼 뒤 PC에서 디코딩했다.
2. 이전에 분석한 현재 앱의 IL2CPP 타입 정보로 `MasterManager.<>c → declaringType → MasterManagerBase<MasterManager> → static_fields → _instance`를 따라갔다.
3. `_downloadedMasterFileInfos`의 사전과 `DownloadMasterFileInfo` 객체들을 읽어 마스터 이름, 저장 경로, 키 byte 배열, 원본 크기를 확보했다. 관련 메모리는 64KiB 단위의 제한된 청크로 저장했으며 전체 프로세스 덤프는 필요하지 않았다.
4. 키 배열은 67바이트의 SQLCipher `x'<64 hex characters>'` 형태다. 가운데 64자리 hex는 원시 256비트 키다. 실제 키 값은 이 문서나 Git 추적 파일에 포함하지 않는다.
5. 원본 파일을 ADB pull로 복사하고, 파일별 복구 키로 각 페이지의 HMAC를 확인한 뒤 복호화했다.
## 검증된 파일 형식
- 페이지 크기: 4,096바이트
- 암호화: AES-256-CBC, 원시 키 32바이트
- 파일 첫 16바이트: salt
- 페이지 끝 reserved 영역: 80바이트 (IV 16 + HMAC 64)
- HMAC: SHA512, 암호문과 IV 뒤에 little-endian 페이지 번호를 포함
- HMAC 키: 원시 DB 키를 입력으로, `salt XOR 0x3A`와 PBKDF2-HMAC-SHA512 2회를 사용해 32바이트 파생
- 첫 페이지의 평문 SQLite 헤더를 복원하고 reserved 영역은 0으로 채운 복사본을 생성했다. 페이지 헤더의 reserved 크기는 유지한다.
설정은 추정으로만 채택한 것이 아니라 전체 페이지 HMAC 일치 및 독립 SQLite 무결성 검사로 검증했다. 암호화와 평문 파일의 SHA-256을 파일별로 남겼다.
## 로컬 산출물 (Git 제외)
모두 `data/inspection/android-master-2026-09-29/` 아래에 있다.
- `master-keys.private.json`: 파일별 키, 경로, 마스터 버전 및 객체 주소. **비공개로 유지할 자료**.
- `memory/`: 키 복구에 사용한 선택적 메모리 덤프. 계정 관련 값이 섞일 수 있으므로 비공개로 유지한다.
- `encrypted-master/`: 암호화된 원본 복사본 379개.
- `decrypted-master/`: 이름을 붙인 평문 SQLite DB 379개 (`Card.sqlite`, `Music.sqlite` 등).
- `decrypt-manifest.json`: 암호 검증 페이지 수, 파일 크기 및 양쪽 SHA-256.
- `sqlite-verification.json`: DB별 무결성 검사, 테이블 스키마와 행 수.
- `memory.py`, `master_instance.py`, `dump_master_keys.py`: 메모리 읽기 및 키 복구 스크립트.
- `decrypt-master.mjs`, `verify_decrypted.py`: 로컬 복사본 복호화와 독립 SQLite 검증 도구.
메모리 주소와 타입 레이아웃은 현재 앱 빌드/프로세스 기준이다. 앱을 재시작하거나 업데이트하면 재확인해야 한다. 확보한 키와 같은 버전의 암호화 파일 복사본은 앱 실행 없이도 로컬에서 다시 검증할 수 있다.
## 수집 기준 확인
작업 전 `npm run data:sync`를 실행하고 `latest.json`, `latest.md`, `changes.patch`를 모두 확인했다. 영문/한국어/일본어 공식 원격은 기존 9월 26일 기준 커밋과 같고, 모든 콘텐츠 분류에서 변경이 없었다. 이번 로컬 게임 스냅샷의 확보가 HolodoriDB 수집 기준을 갱신하지는 않는다.
참고: [SQLCipher 키 API](https://www.zetetic.net/sqlcipher/sqlcipher-api/), [SQLCipher 설계](https://www.zetetic.net/sqlcipher/design/).
@@ -0,0 +1,13 @@
# 이벤트 일정·보너스 설정의 확인 범위 (2026-09-29)
- 현재 직접 확보한 DB 379개에서 이벤트별 시작/종료와 카드·멤버·개화별 특효 설정 테이블을 확인하지 못했다. EventStory/EventStoryChapter는 존재한다.
- 이벤트 관련 모든 값이 DB에 없는 것은 아니다. MarathonLiveRewardUpStaminaEffect, MarathonMiniGameRewardUpStaminaEffect 및 Setting.marathonRecommendDeckBonusPermilMultiply 같은 공통 설정은 DB에 존재한다. 이들은 이벤트별 특효 설정과 구분한다.
- 공식 CDN Octo revision 93 목록 43,749개(번들 24,359개, 리소스 19,390개)를 조사했다. 이벤트·설정 관련 이름 1,580개는 이미지·연출·배경·시나리오·음성 등이었고, 이름으로 식별되는 이벤트 일정/특효 설정 파일은 확인되지 않았다. 번들의 내부를 전부 검사한 것은 아니므로 CDN에 절대 없다는 결론은 내리지 않는다.
- 현재 앱에서 추출한 protobuf 정의에는 rpc.api.EventListEventInfoResponse가 존재한다.
- EventInfo: start_time, end_time, exchange_end_time
- MarathonInfo.MarathonChapter: end_time, marathon_music_score_bonuses, marathon_mini_game_marathon_score_bonuses, marathon_live_marathon_score_bonuses
- MarathonMusicScoreBonus: music_id, character_id, card_id, card_attribute_type, card_rarity, card_potential_upgrade_count, score_up_permil_up
- MarathonLiveMarathonScoreBonus: character_id, card_id, card_attribute_type, card_rarity, card_potential_upgrade_count, marathon_score_quantity_up_permil_up
- 따라서 앱이 이벤트별 일정과 보너스를 서버 API 응답으로 전달받을 수 있는 구조는 확인했다. 실제 응답의 현재 값은 아직 수집하지 않았다. CDN/DB 부재를 이유로 외부 위키가 필수라고 볼 근거는 없다.
로컬 검증 기록: data/inspection/android-master-2026-09-29/cdn-event-catalog.json 및 event-proto-schema.json. 계정 인증 정보는 사용하지 않았다.