#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
mhp3pack — MHP3 クエストの配布形態(PAT / data.bin)への変換ツール
endecryptQST(ネイティブexe)の逆アセンブル解析と readme.txt の構造仕様に基づく。

機能:
  pat     <quest.dat> <out.pat> [--addr 0x0967B440]   平文クエスト → FreeCheat PATメモリパッチ
  unpat   <in.pat> <out.dat>                          PAT → 平文クエスト(.dat)
  db-list <data.bin>                                  平文data.bin内の100スロットmibを一覧
  db-set  <data.bin> <slot> <quest.dat> <out.bin>     指定スロットにクエストを差し込み(平文data.bin)
  db-get  <data.bin> <slot> <out.dat>                 指定スロットのクエストを抽出
  db-new  <out.bin> <quest1.dat> [quest2.dat ...]     空data.binに先頭から詰めて新規作成

PAT/PATの平文クエストは MHP3TaskEdit / mhp3qst.py が生成するDATと同一フォーマット。
data.bin(0x259000)= mib100個(各0x6010)+checksum+key。各mibの+0x20にv005クエストが入る。
※ data.bin の暗号化(→途中データ)とPSPセーブ暗号(→MHP3RD.BIN)は別工程。本書 README参照。
"""
import sys, os, struct, argparse

# --- 定数(endecryptQST readme / 逆アセンブル解析より) ---
QUEST_RAM_ADDR = 0x0967B440      # PAT書き込み先 PSP RAM アドレス(=DLクエスト領域 0x00E7B440)
MIB_SIZE   = 0x6010              # 1 mib のサイズ
MIB_COUNT  = 100
DB_SIZE    = 0x259000            # data.bin 全体サイズ
MIB_KEY_OFF      = 0x0000        # 8 bytes (任意)
MIB_SIZE_OFF     = 0x0008        # u32 クエストデータ長 (<=0x5FD0)
MIB_CKSUM_OFF    = 0x000C        # 20 bytes (暗号化時に自動更新)
MIB_QUEST_OFF    = 0x0020        # クエストデータ本体
MIB_QUEST_MAX    = 0x5FD0
MIB_NAME_OFF     = 0x6000        # 16 bytes mibファイル名(0x6002-0x6005が判定に使用)
DB_CKSUM_OFF     = 0x258FE8      # 20 bytes
DB_KEY_OFF       = 0x258FFC      # 4 bytes (任意)
DAT_MAGIC = bytes.fromhex('94000000763030')  # v005

# ================= PAT =================
def cmd_pat(a):
    quest = open(a.questdat,'rb').read()
    addr = int(a.addr,0) if isinstance(a.addr,str) else a.addr
    out = struct.pack('<I', addr) + quest
    open(a.outpat,'wb').write(out)
    print('PAT生成: %s (%d bytes, addr=0x%08X)' % (a.outpat, len(out), addr))

def cmd_unpat(a):
    raw = open(a.inpat,'rb').read()
    addr = struct.unpack_from('<I', raw, 0)[0]
    open(a.outdat,'wb').write(raw[4:])
    print('PAT展開: addr=0x%08X, quest=%d bytes -> %s' % (addr, len(raw)-4, a.outdat))

# ================= data.bin(平文コンテナ) =================
def _read_db(path):
    d = bytearray(open(path,'rb').read())
    if len(d) < DB_SIZE:
        d += bytes(DB_SIZE - len(d))
    return d

def _mib_base(slot): return slot * MIB_SIZE

def _get_quest(db, slot):
    base = _mib_base(slot)
    size = struct.unpack_from('<I', db, base + MIB_SIZE_OFF)[0]
    if size == 0 or size > MIB_QUEST_MAX:
        return None, size
    return bytes(db[base+MIB_QUEST_OFF : base+MIB_QUEST_OFF+size]), size

def _name(db, slot):
    base = _mib_base(slot)
    nm = bytes(db[base+MIB_NAME_OFF : base+MIB_NAME_OFF+16])
    return nm

def cmd_db_list(a):
    db = _read_db(a.databin)
    print('data.bin: %s (len=%d, 0x%X)' % (a.databin, len(db), len(db)))
    used=0
    for s in range(MIB_COUNT):
        q,size = _get_quest(db, s)
        nm = _name(db,s)
        nm_hex = nm.hex()
        valid_v005 = (q is not None and q[:7]==DAT_MAGIC)
        if size:
            used+=1
            tag = 'v005' if valid_v005 else '????'
            print('  slot%02d size=0x%-4X %s name=%s' % (s, size, tag, nm_hex))
    print('使用スロット: %d / %d' % (used, MIB_COUNT))

def cmd_db_get(a):
    db = _read_db(a.databin)
    q,size = _get_quest(db, a.slot)
    if not q:
        print('slot%d は空です' % a.slot); return
    open(a.outdat,'wb').write(q)
    print('抽出: slot%d (%d bytes) -> %s' % (a.slot, size, a.outdat))

def _put_quest(db, slot, quest, name16=None):
    base = _mib_base(slot)
    if len(quest) > MIB_QUEST_MAX:
        raise SystemExit('クエストが大きすぎます(%d > %d)' % (len(quest), MIB_QUEST_MAX))
    # クリア(mib領域)
    db[base:base+MIB_SIZE] = bytes(MIB_SIZE)
    # size
    struct.pack_into('<I', db, base+MIB_SIZE_OFF, len(quest))
    # quest data
    db[base+MIB_QUEST_OFF : base+MIB_QUEST_OFF+len(quest)] = quest
    # name (0x6000..0x600F) 既存名を保持したい場合は呼び出し側で指定。無指定なら slot連番。
    if name16 is None:
        name16 = ('K%03d' % slot).encode('ascii').ljust(16, b'\x00')
    db[base+MIB_NAME_OFF : base+MIB_NAME_OFF+16] = name16[:16].ljust(16, b'\x00')

def cmd_db_set(a):
    db = _read_db(a.databin)
    quest = open(a.questdat,'rb').read()
    keep = _name(db, a.slot)
    name16 = keep if any(keep) else None
    _put_quest(db, a.slot, quest, name16)
    open(a.outbin,'wb').write(db)
    print('差し込み: slot%d <- %s (%d bytes) -> %s' % (a.slot, a.questdat, len(quest), a.outbin))

def cmd_db_new(a):
    db = bytearray(DB_SIZE)
    for i,qf in enumerate(a.quests):
        if i >= MIB_COUNT:
            print('[警告] 100スロット超過。残りは無視: %s' % qf); break
        _put_quest(db, i, open(qf,'rb').read())
    open(a.outbin,'wb').write(db)
    print('新規data.bin作成: %s (%d クエスト, %d bytes)' % (a.outbin, min(len(a.quests),MIB_COUNT), len(db)))

def main():
    ap = argparse.ArgumentParser(prog='mhp3pack')
    sub = ap.add_subparsers(dest='cmd', required=True)
    p=sub.add_parser('pat'); p.add_argument('questdat'); p.add_argument('outpat'); p.add_argument('--addr',default=hex(QUEST_RAM_ADDR)); p.set_defaults(fn=cmd_pat)
    p=sub.add_parser('unpat'); p.add_argument('inpat'); p.add_argument('outdat'); p.set_defaults(fn=cmd_unpat)
    p=sub.add_parser('db-list'); p.add_argument('databin'); p.set_defaults(fn=cmd_db_list)
    p=sub.add_parser('db-get'); p.add_argument('databin'); p.add_argument('slot',type=int); p.add_argument('outdat'); p.set_defaults(fn=cmd_db_get)
    p=sub.add_parser('db-set'); p.add_argument('databin'); p.add_argument('slot',type=int); p.add_argument('questdat'); p.add_argument('outbin'); p.set_defaults(fn=cmd_db_set)
    p=sub.add_parser('db-new'); p.add_argument('outbin'); p.add_argument('quests',nargs='+'); p.set_defaults(fn=cmd_db_new)
    a=ap.parse_args(); a.fn(a)

if __name__=='__main__':
    main()
