Document verified Android game architecture, rules and development procedures

This commit is contained in:
NINI
2026-10-07 03:15:08 +09:00
parent ea2ad4506c
commit 2d45e11120
30 changed files with 676 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
# 파일과 명령행 계약
현재 게임에는 HTTP API·원격 인증·웹훅이 없다. 외부에서 사용하는 경계는 로컬 저장 JSON과 개발용 명령행 도구다. 경로 인수는 호출한 PC의 파일 경로이며 쉼표로 분리하는 음성 ID 이외에는 경로의 공백을 따옴표로 감싼다.
## 진행 저장 JSON 버전 1
UTF-8 JSON 객체이며 필드 이름은 대소문자를 구별한다. 아래 필수 필드는 모두 있어야 한다. 배열은 `null`을 허용하지 않으며 `latestFreePlay`만 `null`을 허용한다. 중복 JSON 속성·뒤에 붙은 추가 JSON·잘못된 타입·필수 필드 누락은 읽기 오류다. 알 수 없는 추가 필드는 현재 명시적으로 거부하지 않는다. 버전 1 이외의 자동 변환은 구현되어 있지 않다.
| 최상위 필드 | 타입·제약 |
|---|---|
| `schemaVersion` | 정수 1 |
| `completedStageIds` | 중복 없는 정수 배열, 각 값 1~30. 형식 검증이 연속 번호를 요구하지는 않는다. |
| `seenTutorialIds` | 중복 없는 문자열 배열, 값은 `merge` 또는 `split` |
| `puzzleStates` | 아래 퍼즐 객체 배열, 최대 30개, 번호 중복 없음 |
| `latestFreePlay` | 자유 놀이 객체 하나 또는 null |
퍼즐 객체는 `stageId`(정의와 일치하는 정수), `balls`, `groups`, `collectedBallIds`, `countedBallIds`, `camera`를 가진다. 구 개수는 해당 고정 스테이지 총량과 같아야 한다. 수집·계수는 문자열 ID 배열이며 각각 중복 없이 `countedBallIds ⊆ collectedBallIds ⊆ balls.id`다. 구성 퍼즐은 두 배열이 비어 있어야 한다. 완료 기록이 있는 번호의 재플레이 퍼즐도 저장할 수 있다.
자유 놀이 객체는 `sourceStageId`, `balls`, `groups`, `camera`만 갖는다. 수집·계수 필드는 없다. source 번호는 1~30이고 완료 목록에 있어야 한다. 자유 놀이 구는 0~10개다.
| 공통 객체 | 필드·제약 |
|---|---|
| ball | `id`: 비어 있거나 공백뿐이지 않은 고유 문자열. `position`: x/y/z 유한 숫자 객체 |
| group | `id`: 같은 상태 안에서 고유하고 비어 있지 않은 문자열. `ballIds`: 비어 있지 않은 문자열 배열 |
| camera | `target`: x/y/z 유한 숫자. `rotation`: x/y/z/w 유한 숫자. `distance`: 유한 양수 |
구 ID와 묶음 ID는 각 종류 안에서 유일해야 한다. 저장 검증은 ID의 특정 접두사·정규식·길이 상한을 요구하지 않는다. 묶음은 실제 구만 참조하고, 모든 구가 묶음 전체에서 정확히 한 번 등장해야 한다. 빈 보드는 빈 묶음 배열을 가진다. 회전은 quaternion이며 `abs(x²+y²+z²+w²-1) < 0.001`이다. 저장 거리에는 양수 제약을 적용하며 화면 카메라의 현재 3~15 범위와 구분한다.
```json
{
"schemaVersion": 1,
"completedStageIds": [],
"seenTutorialIds": [],
"puzzleStates": [],
"latestFreePlay": null
}
```
읽기는 유효 주 파일 전체 → 유효 `.bak` 전체 순서다. 둘 다 없으면 새 기록으로 성공하며, 주 파일만 무효이고 백업이 유효하면 복구 알림과 백업 상태를 반환한다. 둘 다 무효이면 오류와 초기 상태를 제공하고 손상 원본을 진단 사본으로 보존한다. `.tmp`는 복원 후보가 아니다. 쓰기는 전체 상태 검증 후 임시 파일 flush와 파일 교체로 수행하며 쓰기 실패는 화면에 알린다. 백업은 이전 확정 상태일 수 있으므로 최신 조작 보존을 보장하지 않는다.
## 검증 진입점
```powershell
pwsh -NoProfile -File tools/verify.ps1 -Phase EditMode -Milestone Final
```
`Phase`는 필수이며 `EditMode`, `PlayMode`, `AndroidBuild`, `AndroidSmoke` 중 하나다. `Milestone`은 `FirstSlice`, `Composition`, `Persistence`, `Content`, `Final` 중 하나로 기본값은 `Final`이다. 프로젝트 루트에서 실행한다.
| 단계 | 출력·입력 의존 |
|---|---|
| EditMode·PlayMode | `artifacts/verification/<Milestone>/<phase>.xml`, 로그, `<phase>-evidence.json`. 실제 테스트 결과를 검사한다. |
| AndroidBuild | `artifacts/android/<Milestone>/MagnetismHeroes.apk`, 빌드 로그·APK 메타데이터·SHA. Final은 표준 APK 사본도 만든다. |
| AndroidSmoke | 같은 소스에서 생성된 해당 Milestone APK와 빌드 기록을 요구한다. 실제 설치·입력·상태·음성 원문과 결과 JSON을 남긴다. |
모든 실제 검사와 소스 전후 일치가 통과하면 종료 0, 실패·보고서 누락·빈 테스트·dirty 소스·APK 불일치면 종료 1이다. 잘못된 인수는 PowerShell의 인수 오류와 비정상 종료다. 실패 JSON은 `status: failed`, `error`를 기록할 수 있으므로 파일 존재만으로 통과를 판단하지 않는다. Final Android 하위 검사는 정상 30개 진행과 별도로 경계·빠른 계수·콘텐츠 검사를 실행한다.
APK 음성 별도 검사 호출은 `tools/android/inspect-voice-payload.ps1 -ApkPath <APK> -EvidenceDir <결과폴더>`다. Python과 고정 UnityPy 1.25.4 QA 환경을 준비하고 결과의 `apk-voice-payload.json`에 실제 AudioClip 개수·승인 PCM 비교·APK SHA를 남긴다. 현재 채택 44개와 다르거나 읽을 수 없으면 비정상 종료하며 APK에 도구가 설치되는 것은 아니다.
PCM 비교는 `tools/android/compare-audio.ps1 -ReferencePath <기준WAV> -CapturedPath <녹음WAV> -OutputPath <JSON> -RequireFullSpeech`다. 입력은 PCM16 mono WAV이며 도구 내부 비교는 24kHz로 맞춘다. 출력은 상관·발화 시작·길이·RMS·기준/녹음 SHA·passed와 전체 발화 검사 결과다. 상관 0.95 이상, RMS 32 초과, 비무음 표본 100 초과 및 전체 발화 조건을 만족하면 0, 불일치면 1이다. 지원하지 않는 WAV·무음·입출력 오류는 비정상 종료한다. 전체 발화 인수를 생략한 부분 비교는 잘림 합격 증거로 사용하지 않는다.
## 음성 생성·후보 내보내기
```text
python tools/audio/generate_voice.py --catalog <JSON> --profile <JSON> [--ids id1,id2] [--regenerate]
python tools/audio/generate_voice.py --catalog <JSON> --profile <JSON> --validate-only --manifest <JSON> [--ids id1,id2]
python tools/audio/prepare_voice.py --catalog <JSON> --profile <JSON> --manifest <JSON> [--ids id1,id2] [--target <폴더>]
```
catalog는 `schemaVersion:1`, 비어 있지 않은 `clips` 배열이다. 각 clip은 고유한 `id`(`[a-z][a-z0-9_]*`), 한국어 전체 `text`, 비어 있지 않은 `usage`, 1~30 정수의 비어 있지 않은 `stageIds`, boolean `required`를 가진다. text에 숫자 기호를 넣지 않고 ‘여덟’ 같은 발화 문장을 쓴다. 선택 ID는 중복 없이 모두 catalog에 존재해야 한다.
profile은 `schemaVersion:1`, `modelId`, `modelRevision`, `language`, `speaker`, `instruct`, `device`, `dtype`, `attention`, 정수 `seed`, 객체 `generationParameters`다. 현재 생성 도구는 승인된 Qwen 1.7B CustomVoice·Korean·Sohee와 `cuda:0`을 요구한다. modelRevision은 40자 소문자 hex, dtype은 bfloat16/float16, attention은 sdpa/eager다. 실제 채택 프로필은 bfloat16·sdpa다.
생성은 catalog 옆 `generated/<runId>/`에 원본 WAV·입력 catalog/profile snapshot·manifest를 남긴다. manifest는 입력·대본·설정 해시, 원래 생성 회차, generated/reused/failed 결과, 파형의 실제 sample rate·채널·길이·해시와 환경을 기록한다. `--ids` 생략은 전체 catalog 생성이며 `--validate-only`는 모델을 로드하지 않고 전체 필수 또는 선택 범위를 검사한다. 입력·파일·해시·생성 오류가 없으면 종료 0, 있으면 오류와 종료 1이다. 일치 입력과 파일은 재사용하고 `--regenerate`는 새 후보 회차를 만든다.
prepare는 유효 manifest를 받아 44.1kHz mono PCM16의 `edited/<runId>/`와 selection JSON, target의 WAV를 만든다. 선택을 생략하면 필수 ID를 대상으로 한다. 기본 target은 게임의 Voice 폴더다. 기존 runtime 파일과 바이트가 다르면 기존 파일을 보존하며 오류 종료 1을 낸다. 숫자 발화 원본이 2초를 초과하거나 편집으로 clipping이 생기는 경우도 오류다. 후보 검사 성공은 종료 0이며 selection은 `draft/not_listened`다. 자동으로 사용자 승인이나 active-adoption을 갱신하지 않는다.
## 모델 내보내기
Blender의 `--background --python tools/models/build_playgrounds.py`는 저장소 루트에서 실행한다. 입력은 스크립트의 모델 정의이며 `SourceAssets/Models/PlaygroundProps.blend`와 Presentation/Models의 13개 FBX를 재작성한다. 사용자 인수를 받는 범용 모델 CLI가 아니다. Blender 실행·내보내기 오류는 비정상 종료하며 자동 성공 출력은 Unity의 방향·재질·콜라이더 검수를 의미하지 않는다.