메타포 리판타지오 폰트 변경 모드

0.exe(Metafont, 자동, 추천)

https://mega.nz/file/PC5FBBxY#xwyqpSZhgHvr3SBaOUzBohmD_ImDnqThbvM7dT4AO6M

3.exe(Metafont_b, 반자동)

https://mega.nz/file/LCRHVC6D#iwUgSsmgWtoZHq1hmsQhUE7lzK7wgbkDeyq46zzNCNY


체험판이 나왔을 때 지금까지 한글 패치를 제작해온 경험을 살려 HxD로 폰트 변경하는 방법을 연구해서 처음으로 갤에 공개했던 것은 그냥 누가 모드 만들어주기 전까지 폰트 변경이 필요한 사람은 임시로라도 쓰라는 의미에서 공개한 것이었습니다. 근데 그게 여기저기 퍼져서 모드가 필요 없어져서 그런가 정식 발매 후 일주일이 지나도록 아무도 모드를 안 만들길래 그냥 부족한 실력이나마 직접 (chatgpt 딸깍으로) 만들었습니다. 프로그램 사용으로 인한 어떤 결과도 전 책임지지 않으며 본인 스스로의 판단으로 사용하시길 바랍니다. 문제시 연락주시면 내리겠습니다.

10월 19일 업데이트 : 2.exe를 업그레이드한 3.exe를 추가

10월 21일 업데이트 : 3.exe를 B, C, F도 변경할 수 있게 업그레이드

10월 27일 업데이트 : 1.exe를 업그레이드한 0.exe를 추가, 0.exe, 3.exe를 체험판도 사용 가능하게 수정

10월 31일 업데이트 : 0.exe 진행률 표시 추가, 속도 정상화, 파일 하나로(onefile)





※ 디지털 서명이 없어서 그런가 디펜더에서 트로이 목마 있다고 잡히다가 안 잡히다가 그러는데 시간이 좀 지나면 알아서 풀어주는 거 같네요. 암튼 일단 바이러스 토털 돌려봤는데 괜찮은듯. 그래도 불안하면 파이썬 설치하고 메모장으로 아무 txt파일이나 만든 다음 맨 아래 코드 중 필요한거 골라서 복붙 저장하고 확장자를 py로 바꿔준 다음 설치 폴더에 넣고 똑같이 실행하면 됩니다.

※ 잘 안 되면 그냥 HxD로 수동으로 하시면 됩니다 검색하면 방법 나옴

※ 이 프로그램들은 프로그램 안에 폰트 파일이 있는 것도 아니고 폰트 파일 자체를 임의로 수정하는 것도 아니라 저작권 문제에 비교적 자유롭고, 고정된 위치에 있는 내용을 바꾸는 방식도 아니라서 나중에 공식 업데이트를 해서 파일이 변경되어도 그대로 쓸 수 있음

 ※ 스팀덱, PC 둘 다 사용 가능할 듯? 일단 1.exe는 스팀덱에서 성공했다는 사람이 있는데 나머지는 모름. 스팀덱 없어서 테스트 못하니 안되면 그냥 PC에서 패치하고 옮기세요












0번 : 압축을 설치 폴더에 풀고 0.exe를 실행

자동으로 B폰트의 위치를 찾아서 그걸 A폰트의 위치에 덮어씌우는 방식.

원클릭 딸깍(추천)
폰트를 변경하지 않은 순정 base.cpk만 사용 가능
원본 base.cpk에 덮어씌우는 방식이라 빈 저장 공간이 크게 필요하지 않고 더 빠름.



3번 : 압축을 설치 폴더에 풀고 3.exe를 실행.

1을 입력하고 엔터를 누르면 순정 base.cpk에서 A,B,C,F 각각의 폰트의 위치를 찾고 finfo.txt에 입력함. 이미 수정된 base.cpk에서는 사용하지 말 것. 공식 업데이트시 한번 더 검색해서 갱신할 필요 있음.

1번을 입력한 후 검색을 끝마치거나, 이미 위치가 입력된 finfo.txt가 있어서 2를 입력하면 덮어쓰기 선택 단계로 들어감. 

덮어쓰기 선택 단계에서 2부터 5는 각각 설치 폴더 내의 A.ttf, B.ttf, C.ttf, F.ttf를 각각의 위치에 덮어씌우는 기능. A.ttf는 1,391,608. B.ttf는 908,148. C.ttf와 F.ttf는 907,760 바이트 보다 작아야함. 해당 ttf가 없거나 크면 바로 꺼짐.

숙련자용.
위치만 한번 찾아놓으면 이미 폰트 변경을 해준 base.cpk에도 재사용 가능.
공식 업데이트시 위치 갱신 필요
원본 base.cpk에 덮어씌우는 방식이라 빈 저장 공간이 크게 필요하지 않고 더 빠름.


----------------------------------

여긴 빈 저장공간 85GB 이상이 필요한 구형 버전입니다
혹시 0이나 3에서 에러 뜰 경우 이것을 사용해 주세요.

1.exe, 2.exe(Metafont_a, 자동, 85GB 여유 공간 필요)
https://mega.nz/file/iCQQRJDL#FL4rynctEQHazJFuj7kKnfR9BugOVVcPSgUOkmZgvp0
1번 : 압축을 설치 폴더에 풀고 1.exe를 실행
자동으로 B폰트의 위치를 찾아서 그걸 A폰트의 위치에 덮어씌우는 방식.
2번 : 압축을 설치 폴더에 풀고 2.exe를 실행
자동으로 A폰트의 위치를 찾아서 A.ttf를 그 위치에 덮어씌우는 방식.
설치 폴더에 따로 A.ttf라는 이름의 폰트 파일을 하나 준비해둬야 함. A.ttf는 1,391,608 바이트보다 작아야 함. A.ttf가 없거나 크기가 너무 클 경우 프로그램 실행해도 그냥 꺼짐. 어떤 폰트를 A.ttf로 할지는 자기 나름.
실행시 85기가 이상의 빈 저장공간 필요.
폰트를 변경하지 않은 순정 base.cpk만 사용 가능
※ 주의 : 1.exe, 2.exe를 사용할 경우 base.cpk가 제대로 바뀌었는지 꼭 확인할 것. 이게 modified_base.cpk를 새로 만든 다음에 원본 base.cpk를 삭제하고 modified_base.cpk를 base.cpk로 이름을 바꾸는 방식임. 그런데 base.cpk가 삭제가 안 되고 두 파일이 그대로 남아 있어서 폰트가 안 바뀐 채로 실행되거나, base.cpk는 삭제됐는데 modified_base.cpk의 이름이 안 바뀌고 그대로 있어서 실행이 안 되는 경우가 종종 발생함.


-------------------------------------------

코드들


0번

import threading
import os
import time

# Af와 Bf의 패턴 정의
PATTERNS = {
    "Af": bytes.fromhex("00010000000D0080000300504744454600FB47DF000000DC0000002A4F532F328A068EB20000010800000060636D61702FBC93A5000001680001C80E67617370FFFF00030001C97800000008676C7966140BD84F0001C98000111B"),
    "Bf": bytes.fromhex("00010000000D0080000300504744454600FB47DF000000DC0000002A4F532F328A068EB20000010800000060636D61702FBC93A5000001680001C80E67617370FFFF00030001C97800000008676C7966CFDF48C30001C9800009C6")
}

# 진행률 표시 함수 (출력 주기 설정)
def print_progress(current, total, step=0x1000000):  # 16MB마다 업데이트
    if current % step == 0 or current == total:
        percent = 100 * (current / float(total))
        print(f"\r진행률: {percent:.2f}% ({hex(current)}/{hex(total)})", end='', flush=True)

# 패턴 검색 함수
def search_pattern(file_path, pattern, chunk_size=16 * 1024 * 1024, start_offset=0xA00000000):
    file_size = os.path.getsize(file_path)
    offset = start_offset

    if file_size < 60 * 1024 * 1024 * 1024:  # 파일 크기가 60GB보다 작은 경우
        offset = 0xA00000000
    else:  # 파일 크기가 60GB 이상인 경우
        offset = 0x1000000000

    with open(file_path, "rb") as file:
        while offset < file_size:
            print_progress(offset, file_size)
            file.seek(offset)
            chunk = file.read(chunk_size)
            index = chunk.find(pattern)
            if index != -1:
                return offset + index
            offset += chunk_size
    return None

# Bf2 덮어쓰기 함수
def overwrite_bf2(file_path, af_position, bf_position):
    try:
        with open(file_path, "r+b") as file:
            file.seek(bf_position)
            bf2_data = file.read(0xDDB74)  # Bf2 데이터 크기

            # 데이터의 크기가 충분하지 않으면 오류 발생
            if len(bf2_data) < 0xDDB74:
                print("\nBf2 데이터의 크기가 0xDDB74보다 작습니다.")
                return

            file.seek(af_position)
            file.write(bf2_data)  # Af 시작 위치에 Bf2 데이터 덮어쓰기
        print("\nAf 위치에 Bf2 데이터가 덮어씌워졌습니다.")
    except Exception as e:
        print(f"\n오류 발생: {e}")

def wait_and_exit():
    time.sleep(5)
    print("\n프로그램이 자동으로 종료됩니다.")
    os._exit(0)

def main():
    file_path = "base.cpk"
    if not os.path.exists(file_path):
        print(f"{file_path} 파일이 존재하지 않습니다.")
        input("문제를 확인하려면 엔터를 누르세요...")
        return

    # Af와 Bf의 위치를 검색
    chunk_size = 16 * 1024 * 1024  # 기본 청크 크기
    while True:
        af_position = search_pattern(file_path, PATTERNS["Af"], chunk_size)
        bf_position = search_pattern(file_path, PATTERNS["Bf"], chunk_size)

        if af_position is None or bf_position is None:
            print("\n이미 폰트가 변경된 것 같습니다.")
            choice = input("다른 청크 크기로 검색하시겠습니까? (1: 예, 2: 아니오): ")
            if choice == "1":
                chunk_size = int(input("새 청크 크기를 MB 단위로 입력하세요: ")) * 1024 * 1024
                print(f"\n청크 크기가 {chunk_size // (1024 * 1024)}MB로 설정되었습니다. 다시 검색을 시작합니다.")
            else:
                print("검색을 종료합니다.")
                input("문제를 확인하려면 엔터를 누르세요...")
                return
        else:
            break

    # Bf2를 Af 위치에 덮어쓰기
    overwrite_bf2(file_path, af_position, bf_position)

    # 덮어쓰기가 완료되면 완료 메시지 출력 후 대기
    print("\n작업이 완료되었습니다. 프로그램이 5초 후에 종료됩니다...")

    # 엔터 입력을 기다리는 스레드 시작
    timer_thread = threading.Thread(target=wait_and_exit)
    timer_thread.start()

    # 엔터를 기다리는 메인 스레드
    input("계속하려면 엔터를 누르세요...\n")
    timer_thread.join()

if __name__ == "__main__":
    main()



  
  

  
1번

import os
# 패턴 정의
FIRST_PATTERN = bytes.fromhex(
    "00 01 00 00 00 0D 00 80 00 03 00 50 47 44 45 46 00 FB 47 DF 00 00 00 DC "
    "00 00 00 2A 4F 53 2F 32 8A 06 8E B2 00 00 01 08 00 00 00 60 63 6D 61 70 "
    "2F BC 93 A5 00 00 01 68 00 01 C8 0E 67 61 73 70 FF FF 00 03 00 01 C9 78 "
    "00 00 00 08 67 6C 79 66 CF DF 48 C3 00 01 C9 80 00 09 C6 28 68 65 61"
)
SECOND_PATTERN = bytes.fromhex(
    "00 01 00 00 00 0D 00 80 00 03 00 50 47 44 45 46 00 FB 47 DF 00 00 00 DC "
    "00 00 00 2A 4F 53 2F 32 8A 06 8E B2 00 00 01 08 00 00 00 60 63 6D 61 70 "
    "2F BC 93 A5 00 00 01 68 00 01 C8 0E 67 61 73 70 FF FF 00 03 00 01 C9 78 "
    "00 00 00 08 67 6C 79 66 14 0B D8 4F 00 01 C9 80 00 11 1B"
)
# Bfont 데이터 크기 (0xDDB74)
Bfont_size = 0xDDB74
# 파일 경로
input_file = 'base.cpk'
output_file = 'modified_base.cpk'
def find_and_replace(input_file, output_file):
    chunk_sizes = [16 * 1024 * 1024, 32 * 1024 * 1024, 64 * 1024 * 1024, 7 * 1024 * 1024]  # 청크 크기 목록
    for chunk_size in chunk_sizes:
        print(f"\n청크 크기: {chunk_size // (1024 * 1024)}MB")
        
        file_size = os.path.getsize(input_file)
        total_chunks = (file_size + (chunk_size - 1)) // chunk_size  # 청크 수
        with open(input_file, 'rb') as infile, open(output_file, 'wb') as outfile:
            chunk_count = 0
            found_first_pattern = False
            found_second_pattern = False
            while True:
                chunk = infile.read(chunk_size)
                if not chunk:
                    break
                # 첫 번째 패턴 찾기
                first_pattern_index = chunk.find(FIRST_PATTERN)
                if first_pattern_index != -1:
                    found_first_pattern = True
                    # Bfont 데이터를 가져와서 덮어쓰기
                    bfont_data = chunk[first_pattern_index:first_pattern_index + Bfont_size]
                    # 두 번째 패턴을 찾아서 Bfont로 덮어씌우기
                    second_pattern_index = chunk.find(SECOND_PATTERN)
                    if second_pattern_index != -1:
                        found_second_pattern = True
                        # 덮어쓰기
                        chunk = (
                            chunk[:second_pattern_index] +
                            bfont_data +
                            chunk[second_pattern_index + Bfont_size:]
                        )
                outfile.write(chunk)
                chunk_count += 1
                # 진행률 표시
                progress = (chunk_count / total_chunks) * 100
                print(f'\r진행률: {progress:.2f}%', end='')
            if found_first_pattern and found_second_pattern:
                print("\n작업 완료!")
                break
            else:
                print("\n패턴을 찾지 못했습니다. 청크 크기를 증가시킵니다.")
    else:
        print("모든 청크 크기로 패턴을 찾지 못했습니다.")
    # 원본 파일 삭제 및 수정된 파일 이름 변경
    if found_first_pattern and found_second_pattern:
        os.remove(input_file)  # 원본 파일 삭제
        os.rename(output_file, input_file)  # 수정된 파일 이름 변경
# 실행
find_and_replace(input_file, output_file)










2번

import os
import sys

# 패턴 정의
pattern = bytes.fromhex(
    "00 01 00 00 00 0D 00 80 00 03 00 50 47 44 45 46 00 FB 47 DF 00 00 00 DC "
    "00 00 00 2A 4F 53 2F 32 8A 06 8E B2 00 00 01 08 00 00 00 60 63 6D 61 70 "
    "2F BC 93 A5 00 00 01 68 00 01 C8 0E 67 61 73 70 FF FF 00 03 00 01 C9 78 "
    "00 00 00 08 67 6C 79 66 14 0B D8 4F 00 01 C9 80 00 11 1B"
)

# 청크 크기 설정
chunk_sizes = [16 * 1024 * 1024, 32 * 1024 * 1024, 7 * 1024 * 1024]
file_path = "base.cpk"
modified_file_path = "modified_base.cpk"
ttf_file_path = "A.ttf"
max_ttf_size = 0x154000

# 진행률 표시 함수
def print_progress(current, total):
    percent = (current / total) * 100
    sys.stdout.write(f"\r진행률: {percent:.2f}%")
    sys.stdout.flush()

# 패턴 찾기 및 파일 수정
def modify_cpk():
    try:
        with open(file_path, "rb") as f:
            # A.ttf 파일 크기 확인
            ttf_size = os.path.getsize(ttf_file_path)
            if ttf_size &gt; max_ttf_size:
                print("A.ttf 파일이 너무 큽니다. 최대 허용 크기는 0x154000입니다.")
                return

            # 패턴 찾기
            for chunk_size in chunk_sizes:
                f.seek(0)
                current_pos = 0
                while True:
                    chunk = f.read(chunk_size)
                    if not chunk:
                        break
                    index = chunk.find(pattern)
                    if index != -1:
                        pattern_pos = current_pos + index
                        break
                    current_pos += len(chunk)
                    print_progress(current_pos, os.path.getsize(file_path))
                else:
                    continue
                break
            else:
                print("\n패턴을 찾을 수 없습니다.")
                return

            # 수정된 파일 생성
            with open(modified_file_path, "wb") as out_f:
                f.seek(0)
                copied_size = 0

                while copied_size &lt; pattern_pos:
                    chunk = f.read(min(chunk_size, pattern_pos - copied_size))
                    out_f.write(chunk)
                    copied_size += len(chunk)
                    print_progress(copied_size, os.path.getsize(file_path))

                # A.ttf 데이터를 덮어쓰기
                with open(ttf_file_path, "rb") as ttf_f:
                    ttf_data = ttf_f.read()
                    out_f.write(ttf_data)

                # 나머지 데이터 복사
                f.seek(pattern_pos + len(ttf_data))
                while True:
                    chunk = f.read(chunk_size)
                    if not chunk:
                        break
                    out_f.write(chunk)
                    print_progress(out_f.tell(), os.path.getsize(file_path))

        # 파일 이름 변경
        os.remove(file_path)
        os.rename(modified_file_path, file_path)
        print("\n파일 수정이 완료되었습니다.")
    except Exception as e:
        print(f"오류가 발생했습니다: {e}")

# 프로그램 실행
modify_cpk()









3번

import os

# 패턴 정의
PATTERNS = {
    "Af": bytes.fromhex("00010000000D0080000300504744454600FB47DF000000DC0000002A4F532F328A068EB20000010800000060636D61702FBC93A5000001680001C80E67617370FFFF00030001C97800000008676C7966140BD84F0001C98000111B"),
    "Bf": bytes.fromhex("00010000000D0080000300504744454600FB47DF000000DC0000002A4F532F328A068EB20000010800000060636D61702FBC93A5000001680001C80E67617370FFFF00030001C97800000008676C7966CFDF48C30001C9800009C6"),
    "Cf": bytes.fromhex("00010000000D0080000300504744454600FB47DF000000DC0000002A4F532F328A068EB20000010800000060636D61702FBC93A5000001680001C80E67617370FFFF00030001C97800000008676C79661F93162A0001C9800009C4"),
    "Ff": bytes.fromhex("00010000000D0080000300504744454600FB47DF000000DC0000002A4F532F328A068EB20000010800000060636D61702FBC93A5000001680001C80E67617370FFFF00030001C97800000008676C79661F93162A0001C9800009C4")
}

FONT_SIZE_LIMITS = {
    "A.ttf": 1391608,
    "B.ttf": 908148,
    "C.ttf": 907760,
    "F.ttf": 907760
}

def print_progress(current, total):
    percent = 100 * (current / float(total))
    print(f"\r진행률: {percent:.2f}% ({hex(current)}/{hex(total)})", end='')

def search_pattern(file_path, pattern, chunk_size=16 * 1024 * 1024, start_offset=0xA00000000):
    positions = []
    file_size = os.path.getsize(file_path)
    with open(file_path, "rb") as file:
        offset = start_offset
        while offset < file_size:
            print_progress(offset, file_size)
            file.seek(offset)
            chunk = file.read(chunk_size)
            index = chunk.find(pattern)
            if index != -1:
                positions.append(offset + index)
                break  # 하나 찾으면 다음 패턴으로 넘어감
            offset += chunk_size
    return positions

def search_ff_pattern_reverse(file_path, pattern, chunk_size=16 * 1024 * 1024):
    positions = []
    file_size = os.path.getsize(file_path)
    with open(file_path, "rb") as file:
        offset = file_size - chunk_size
        while offset >= 0:
            print_progress(offset, file_size)
            file.seek(offset)
            chunk = file.read(chunk_size)
            index = chunk.find(pattern)
            if index != -1:
                positions.append(offset + index)
                break  # 하나 찾으면 다음으로 넘어감
            offset -= chunk_size
    return positions

def find_font_positions(file_path, chunk_size=16 * 1024 * 1024):
    positions = {}
    for font_name, pattern in PATTERNS.items():
        print(f"\n{font_name} 위치 검색 중...")
        if font_name == "Ff":
            positions[font_name] = search_ff_pattern_reverse(file_path, pattern, chunk_size)
        else:
            positions[font_name] = search_pattern(file_path, pattern, chunk_size)

        if not positions[font_name]:
            print(f"{font_name}가 이미 변경된 것 같습니다.")
            choice = input("청크 크기를 바꿔서 재검색하시겠습니까? (1: 예, 2: 아니오): ")
            if choice == "1":
                new_chunk_size = input("새 청크 크기를 입력하세요 (MB 단위): ")
                if new_chunk_size.isdigit():
                    positions[font_name] = search_pattern(file_path, pattern, chunk_size=int(new_chunk_size) * 1024 * 1024)

    # Cf와 Ff의 패턴이 동일한 경우, 검색된 위치를 정렬하고 할당
    if positions["Cf"] and positions["Ff"]:
        if positions["Cf"][0] == positions["Ff"][0]:
            print("Cf와 Ff의 위치가 동일합니다. 재검색이 필요합니다.")
            new_chunk_size = input("새 청크 크기를 입력하세요 (MB 단위): ")
            if new_chunk_size.isdigit():
                positions = find_font_positions(file_path, chunk_size=int(new_chunk_size) * 1024 * 1024)

    return positions

def load_positions_from_file(filename):
    positions = {}
    if os.path.exists(filename):
        with open(filename, "r") as f:
            for line in f:
                font_name, pos_list = line.split(": ")
                positions[font_name.strip()] = [int(pos.strip().strip("'"), 16) for pos in pos_list.strip()[1:-1].split(", ")]
    return positions

def overwrite_font(file_path, font_type, positions):
    if font_type == 2:  # A to 커스텀
        font_file = "A.ttf"
        target_position = positions["Af"][0] if "Af" in positions else None
    elif font_type == 3:  # B to 커스텀
        font_file = "B.ttf"
        target_position = positions["Bf"][0] if "Bf" in positions else None
    elif font_type == 4:  # C to 커스텀
        font_file = "C.ttf"
        target_position = positions["Cf"][0] if "Cf" in positions else None
    elif font_type == 5:  # F to 커스텀
        font_file = "F.ttf"
        target_position = positions["Ff"][0] if "Ff" in positions else None
    else:
        print("유효하지 않은 덮어쓰기 선택입니다.")
        return

    if os.path.getsize(font_file) > FONT_SIZE_LIMITS[font_file]:
        choice = input(f"{font_file}의 크기가 너무 큽니다. 계속하시겠습니까? (1: 예, 2: 아니오): ")
        if choice != "1":
            print("작업이 취소되었습니다.")
            return

    try:
        with open(font_file, "rb") as f:
            dest_pattern = f.read()  # 파일을 읽어 데이터 그대로 가져오기

        # base.cpk 파일에서 지정된 위치에 덮어쓰기 수행
        if target_position is not None:
            with open(file_path, "r+b") as file:
                file.seek(target_position)
                file.write(dest_pattern)  # 원래 패턴 길이에 상관없이 덮어쓰기
            print(f"{file_path}에서 {font_file}가 덮어씌워졌습니다.")
        else:
            print("대상 위치를 찾을 수 없습니다.")
    except Exception as e:
        print(f"오류 발생: {e}")
        input("문제를 확인하려면 엔터를 누르세요...")

def main():
    file_path = "base.cpk"
    if not os.path.exists(file_path):
        print(f"{file_path} 파일이 존재하지 않습니다.")
        input("문제를 확인하려면 엔터를 누르세요...")
        return

    positions = {}
    choice = input("위치를 검색하시겠습니까?(1: 예, 2: 아니오): ")
    if choice == "1":
        positions = find_font_positions(file_path)
        with open("finfo.txt", "w") as f:
            for font_name, pos_list in positions.items():
                hex_positions = [hex(pos) for pos in pos_list]
                f.write(f"{font_name}: {hex_positions}\n")
        print("\n검색된 위치는 finfo.txt에 저장되었습니다.")
    else:
        positions = load_positions_from_file("finfo.txt")
        if not positions:
            print("finfo.txt 파일에 위치 정보가 없습니다.")
            input("문제를 확인하려면 엔터를 누르세요...")
            return

    while True:  # 덮어쓰기 선택 단계 반복
        font_type = int(input("덮어쓰기 선택 단계 (2: A to 커스텀, 3: B to 커스텀, 4: C to 커스텀, 5: F to 커스텀, 6: 종료): "))
        if font_type == 6:  # 프로그램 종료
            print("프로그램을 종료합니다.")
            break
        overwrite_font(file_path, font_type, positions)
        print("\n작업이 완료되었습니다.")

if __name__ == "__main__":
    main()