신비한 바다의 나디아 한글 패치
MCP로 초벌 작업을 하면서 확실히 느낀 게 있다.
고유번호 작성, 대사 스크립트, 타이틀 로고 재배치 같은 단순 작업은 하위 모델에서 스킬과 하네스로 꽉 잡아서 진행하였다.
하나의 매우 작은 agent.md는 공유하지만 개별적인 내용은 그 에이전트 모델만 담당하여 다른 존재를 확인하지 못한다. 나눠진 에이전트 모델들은 연관성이 절대 없는 독립적인 기능만 구현하도록 분업해 놓았기에 별 문제가 없었다.
근데 확실히 AI가 발전할수록 상위 모델에서의 스킬과 하네스도 점점 무의미해지는 것 같다. 오히려 이러한 제한이 기존 모델의 성능, 토큰, 컨텍스트만 잡아먹는 병목이 되는 것 같기도 하다.
상위 모델에서는 입력 시스템 구현, 렌더러 코드 변경, 최적화를 담당했다.
그래도 생각보다 처음에 나온 코드는 문제가 굉장히 심했다. 여전히 아직까지는 과설계, 과검증이 심하다고 느낀다.
몇 가지 이야기하자면, 먼저 사람이 이해하기 힘들게 작성하는 것이다. 한 번만 사용해야 할 값도 따로 라벨링을 거쳐 복잡하게 만든다.
일반적으로 코드 패치를 할 때는 피연산자일 경우 그 값만 “.org”에 워드 단위로 작성하는 게 아니라, 명령어와 함께 한 라인에 적고 간단하게 주석 처리하면 될 것인데, 지침을 줘도 수정이 필요한 값만 워드 단위로 기재해 놓거나 명령어 자체를 헥스 코드를 쓰는 것이였다.
코드 케이브에서는 많은 라벨링을 거쳐서 선형적으로 적어야 할 코드가 조건 분기도 아닌데 JMP를 써서 쓸데없이 코드 계층이 깊어지는 것까지 확인했다.
무엇보다 가장 황당했던 순간은 간단하게 데이터로 있어야 할 것이 런타임에서 그 구조를 만드는 코드를 보았다.
물론 한글화란 게 돌아가면 그만이지만, 쓸데없는 코드로 오버헤드가 커져 성능에 영향을 주는 것들은 확실히 고쳐야 한다고 생각한다. 특히 고전 게임기 같은 경우는 말이다.
만약 이런 부분을 무시하고 그간 작업했다면, 파일 시스템이나 CD-ROM을 사용하는 게임기에서도 로딩 속도를 고려하지 않고
부트 후 초기화 단계에서 빌트인에서 작성한 코드를 메모리에 미리 올리는 대신, 파일 I/O 커맨드나 최적화되지 않은 고전 BIOS의 함수를 남발했을 것이다.
내가 결과적으로 원하는 MCP는 단순히 “해줘”만으로는 아직까진 수많은 문제가 있으며, 프로그래머가 쉽게 확인하고 관리가 가능한 MCP를 만드는 것이 목적이다.
따라서 조금 더 생각하고 업데이트할 필요가 있을 것 같다.
현재까지 확실히 가능한 것들을 말해보자면
그렇게 까다롭지 않는 매퍼는 변환이 가능하다.
그다음은 고유번호와 대사를 뽑고, 이 롬에서 기존 CHR-ROM 방식을 쓸지 CHR-RAM 방식을 쓸지 판단하는 것이다.
예를 들어 대사가 적고 경우의 수가 얼마 없다면, CHR-ROM 방식을 채택해 필요한 CHR 타일 세트를 미리 만든다.
반대로 대사가 많으면 CHR-ROM 타일 세트를 일일이 구성하기 어렵다. 따라서 CHR-RAM으로 변환하고, 필요한 글자를 동적으로 갱신하면서 고정적으로 쓰는 글자는 인덱스를 재사용한다.
일단 롬을 CHR-RAM을 사용하는 구조로 변경하고, 해당 매퍼에서 지원하는 최대 용량까지 확장한다. 기존 CHR-ROM 데이터는 PRG-ROM으로 옮기고, 부트 후 초기화 단계에서 CHR-RAM에 복사한다. 이후 렌더러 코드를 수정해 한글 폰트를 출력한다.
아직 제대로 해결하지 못하는 것들을 난이도 순으로 나열해 보면
-
CPU 사이클을 계산해 VBlank 안에 몇 타일을 전송해야 최적의 결과가 나오는지 판단하지 못한다. NMI에서 처리하는 다른 작업까지 고려해야 하는데, 전송량을 지나치게 작게 잡으면서 화면을 표시할 때까지 불필요하게 많은 프레임을 기다리게 된다.
-
고정적으로 재사용해야 할 글자와 그때그때 갱신해야 할 글자를 구분하지 못한다. 이를 해결하지 못하면 원본 롬보다 표시 속도가 크게 느려진다. 특히 기존 8×8에서 8×16 폰트로 바뀌면 글자당 전송량이 두 배가 되어 문제가 더 심해진다. 숫자처럼 표시할 값이 바뀌더라도 글리프 자체는 같은 경우, 타일을 다시 전송하지 않고 배치할 인덱스만 바꾸면 된다. 이런 부분은 고정 인덱싱을 사용하거나 CHR-RAM에 미리 올려둔 타일을 재사용해야 한다.
-
가장 심각한 문제는 현재 매핑된 CHR 뱅크에서 더 이상 사용할 공간이 부족해서 스캔라인 IRQ로 화면 구간에 따라 뱅크를 잠깐 전환해서 출력해야 할 때다. 해당 구간에 필요한 폰트는 알아도 같은 구간에 함께 표시되는 배경이나 창 테두리 등의 그래픽이 어떤 타일을 사용하는지 파악하지 못한다. 폰트만 생각하고 뱅크를 전환하면 그 구간의 다른 그래픽이 깨지는 것이다.
이 부분이 지금 가장 큰 난관이며, 어떻게 해결해야 할지 잘 모르겠다. 무엇보다 시간과 토큰 낭비가 너무 심하다.
아무튼 이런 고생을 하면서 더러운 코드지만 관리가 어느 정도 가능한 패치를 만들었다. 확실히 NES의 6502 코드는 상위 기종에서 컴파일러가 생성한 코드와 달리 사람이 직접 작성한 경우가 많아서, 오히려 작성자의 의도와 코드 흐름이 더 직관적으로 드러나는 것 같다.
추가로 게임 중 스탯을 바로 확인할 수 있도록 루아 스크립트를 IPC 브릿지로 파이썬에서 아군과 적군의 스탯 값을 실시간으로 확인하는 것도 재미 삼아 만들어봤다. 이외에 인터넷에 떠도는 게임지니 코드나 디버그, 숨겨진 요소들도 조금 확인해봤는데, 기존 원본 롬에서도 제대로 적용되지 않는 경우가 있는 것 같아 귀찮아서 더 분석하지 않았다.
매번 구조가 다른 롬을 하나 하나 분석하기보다, 자신만의 표준화된 방식을 정립하고 이를 활용하는 방향으로 개발한다면 더 나은 결과로 이어질 거라 생각한다.
스크린 샷

구현된 방식
-
nes2.0 변경 및 CHR-ROM → CHR-RAM 변환 후 ROM 레이아웃
$00~$0F : 원본 PRG 실행 코드/데이터 $10~$1F : 원본 CHR-ROM 그래픽 데이터 $20~$21 : 코드 케이브 $22~$26 : 문자열 포인터,문자열 데이터 $27~$30 : 12x12 한글 폰트 kr_font.bin $31 : 초성, 중성, 종성 테이블 hangul_keys.bin $32 : 조합값에 대응하는 완성형 한글 고유번호 테이블 hangul_codes.bin $33~3D : 빈 영역 $3E~$3F : 고정 뱅크 - 8x8 -> 8x16 폰트로 변경
- 초성 중성 종성 입력 시스템 구현
- 좌표 수정, 메뉴창 크기 수정
- 동적 갱신 + 고정 타일 인덱싱 조합으로 부분 최적화
- 조사 구현
간단한 게임 공략
- 50 면을 클리어하면 55까지 맵이 개방되며 모두 클리어해야 엔딩이 나옴
- CPU 턴에서 생각을 많이 하는 턴이 있는데 원본 롬도 똑같으며 프리징이 아니므로 배속 기능 활용할 것
- 맵에서 사용하는 기술들은 죽여도 경험치를 얻지 못함
- 롱 기술을 사용해서 맵에서 체력을 깍아놓고 마무리는 이동력이 좋은 캐릭터 활용하여 막타를 챙겨 성장하는 방식을 추천
캐릭터
| 아군 캐릭터 | HP | MP | ST | AC | MD | 이동 | 보유 기술 전체 |
|---|---|---|---|---|---|---|---|
| 나디아 (ナディア) | C | B | B | B | B | 주변 1칸 | 프리즈, 전체 챰 |
| 엘렉트라 (エレクトラ) | A | B | C | A | B | 주변 2칸 | 컨퓨즈, 전체 사일런스, 전체 힐 |
| 네모 (ネモ) | B | B | A | C | B | 주변 2칸 | 프리즈, 롱, 멀티, 블로우 |
| 흰고래 (しろくじら) | B | B | C | C | B | 직선 무제한 | 챰, 전체 힐 |
| 남극기지 (なんきょくきち) | B | B | B | C | B | 대각선 무제한 | 전체 컨퓨즈, 테이크 |
| 노틸러스 (ノーチラス) | B | B | A | C | B | 주변 2칸 | 롱, 멀티 |
| 그라탱 (グラタン) | B | B | A | B | C | 주변 1칸 | 롱 |
| 장 (ジャン) | B | B | A | B | B | 주변 1칸 | 멀티 |
| 킹 (キング) | A | B | B | A | B | 주변 1칸 | 힐 |
| 그란디스 (グランディス) | A | B | B | B | B | 주변 1칸 | 사일런스 |
| 적 캐릭터 | HP | MP | ST | AC | MD | 이동 | 보유 기술 전체 |
|---|---|---|---|---|---|---|---|
| 황제 (こうてい) | C | B | B | B | B | 주변 1칸 | 프리즈, 전체 챰 |
| 측근 (そっきん) | A | B | C | A | B | 주변 2칸 | 컨퓨즈, 전체 사일런스, 전체 힐 |
| 가고일 (ガーゴイル) | B | B | A | C | B | 주변 2칸 | 프리즈, 롱, 멀티, 블로 |
| 오르세트라스 (オルセトラス) | B | B | C | C | B | 직선 무제한 | 챰, 전체 힐 |
| 바벨의 탑 (バベルのとう) | B | B | B | C | B | 대각선 무제한 | 전체 컨퓨즈, 테이크 |
| 가피시 (ガーフィッシュ) | B | B | A | C | B | 주변 2칸 | 롱, 멀티 |
| 인간 탱크 (にんげんタンク) | B | B | A | B | C | 주변 1칸 | 롱 |
| 게 로봇 (カニロボット) | B | B | A | B | B | 주변 1칸 | 멀티 |
| 다이소닉스 (ダイソニクス) | A | B | B | A | B | 주변 1칸 | 힐 |
| 대장 (たいちょう) | A | B | B | B | B | 주변 1칸 | 사일런스 |
기술 목록
| 기술 | MP | 효과 |
|---|---|---|
| 프리즈(フリーズ) | 10 | 적 1명 행동불능 |
| 롱(ロング) | 8 | 적 1명 강공격 / 필드 장거리 공격 |
| 힐(ヒール) | 10 | 아군 1명 HP 회복 |
| 전체 힐(タオヒール) | 15 | 아군 전체 또는 주변 아군 HP 회복 |
| 챰(チャム) | 5 | 적 1명 행동불능 |
| 전체 챰(タオチャム) | 8 | 적 전체 행동불능 |
| 컨퓨즈(コンフ) | 6 | 적 1명 혼란 |
| 전체 컨퓨즈(タオコンフ) | 10 | 적 전체 혼란 |
| 사일런스(サイレス) | 5 | 적 1명 기술 봉인 |
| 전체 사일레스(タオサイレス) | 8 | 적 전체 기술 봉인 |
| 테이크(テイク) | 2 | 적 MP 흡수 |
| 멀티(マルチ) | 15 | 적 전체 공격 |
| 블로우(ブロー) | 35 | 적 1명 다단 강공격 |
아이템 목록
| 아이템 | 효과 |
|---|---|
| 일렉테일(エレキテル) | 필드상의 모든 적에게 피해 |
| 박스 (ボックス) | MP 회복 |
| 리프 (リーフ) | HP 회복 |
| 샷건 (ショットガン) | 적 전체 공격 |
| 부스터 (ブースター) | 직접 공격 피해 2배 |
| 포드 (ポッド) | 전투에서 100% 도주 |
| 블루워터 (ブルーウォーター) | 전체 챰, 전체 컨퓨즈, 전체 사일런스, 멀티, 전체 힐 중 하나가 랜덤으로 발동 |
치트
구조체 크기는 0x1C 간격
| 캐릭터 | BASE | LEVEL | ST | AC | EXP | 현재 HP | 현재 MP | 최대 HP | 최대 MP | MD | +11~+1B |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 나디아 | 6000-6002 | 6003 | 6004 | 6005 | 6006-6007 | 6008-6009 | 600A-600B | 600C-600D | 600E-600F | 6010 | 6011-601B |
| 엘렉트라 | 601C-601E | 601F | 6020 | 6021 | 6022-6023 | 6024-6025 | 6026-6027 | 6028-6029 | 602A-602B | 602C | 602D-6037 |
| 네모 | 6038-603A | 603B | 603C | 603D | 603E-603F | 6040-6041 | 6042-6043 | 6044-6045 | 6046-6047 | 6048 | 6049-6053 |
| 흰고래 | 6054-6056 | 6057 | 6058 | 6059 | 605A-605B | 605C-605D | 605E-605F | 6060-6061 | 6062-6063 | 6064 | 6065-606F |
| 남극기지 | 6070-6072 | 6073 | 6074 | 6075 | 6076-6077 | 6078-6079 | 607A-607B | 607C-607D | 607E-607F | 6080 | 6081-608B |
| 노틸러스 | 608C-608E | 608F | 6090 | 6091 | 6092-6093 | 6094-6095 | 6096-6097 | 6098-6099 | 609A-609B | 609C | 609D-60A7 |
| 그라탱 | 60A8-60AA | 60AB | 60AC | 60AD | 60AE-60AF | 60B0-60B1 | 60B2-60B3 | 60B4-60B5 | 60B6-60B7 | 60B8 | 60B9-60C3 |
| 장 | 60C4-60C6 | 60C7 | 60C8 | 60C9 | 60CA-60CB | 60CC-60CD | 60CE-60CF | 60D0-60D1 | 60D2-60D3 | 60D4 | 60D5-60DF |
| 킹 | 60E0-60E2 | 60E3 | 60E4 | 60E5 | 60E6-60E7 | 60E8-60E9 | 60EA-60EB | 60EC-60ED | 60EE-60EF | 60F0 | 60F1-60FB |
| 그란디스 | 60FC-60FE | 60FF | 6100 | 6101 | 6102-6103 | 6104-6105 | 6106-6107 | 6108-6109 | 610A-610B | 610C | 610D-6117 |
| 적군 | BASE | LEVEL | ST | AC | EXP | 현재 HP | 현재 MP | 최대 HP | 최대 MP | MD | +11~+1B |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 황제 | 6118-611A | 611B | 611C | 611D | 611E-611F | 6120-6121 | 6122-6123 | 6124-6125 | 6126-6127 | 6128 | 6129-6133 |
| 측근 | 6134-6136 | 6137 | 6138 | 6139 | 613A-613B | 613C-613D | 613E-613F | 6140-6141 | 6142-6143 | 6144 | 6145-614F |
| 가고일 | 6150-6152 | 6153 | 6154 | 6155 | 6156-6157 | 6158-6159 | 615A-615B | 615C-615D | 615E-615F | 6160 | 6161-616B |
| 오르세트라스 | 616C-616E | 616F | 6170 | 6171 | 6172-6173 | 6174-6175 | 6176-6177 | 6178-6179 | 617A-617B | 617C | 617D-6187 |
| 바벨의탑 | 6188-618A | 618B | 618C | 618D | 618E-618F | 6190-6191 | 6192-6193 | 6194-6195 | 6196-6197 | 6198 | 6199-61A3 |
| 가피쉬 | 61A4-61A6 | 61A7 | 61A8 | 61A9 | 61AA-61AB | 61AC-61AD | 61AE-61AF | 61B0-61B1 | 61B2-61B3 | 61B4 | 61B5-61BF |
| 인간탱크 | 61C0-61C2 | 61C3 | 61C4 | 61C5 | 61C6-61C7 | 61C8-61C9 | 61CA-61CB | 61CC-61CD | 61CE-61CF | 61D0 | 61D1-61DB |
| 게로봇 | 61DC-61DE | 61DF | 61E0 | 61E1 | 61E2-61E3 | 61E4-61E5 | 61E6-61E7 | 61E8-61E9 | 61EA-61EB | 61EC | 61ED-61F7 |
| 다이소닉스 | 61F8-61FA | 61FB | 61FC | 61FD | 61FE-61FF | 6200-6201 | 6202-6203 | 6204-6205 | 6206-6207 | 6208 | 6209-6213 |
| 대장 | 6214-6216 | 6217 | 6218 | 6219 | 621A-621B | 621C-621D | 621E-621F | 6220-6221 | 6222-6223 | 6224 | 6225-622F |
local M = emu.memType.nesMemory
local function write8(addr, value)
emu.write(addr, value, M)
end
local function write16(addr, value)
write8(addr, value & 0xFF)
write8(addr + 1, (value >> 8) & 0xFF)
end
local function writeIf(writer, addr, value)
if value ~= nil then
writer(addr, value)
end
end
local function applyCharacter(base, st, ac, exp, curHp, curMp, maxHp, maxMp, md)
writeIf(write8, base + 0x04, st)
writeIf(write8, base + 0x05, ac)
writeIf(write16, base + 0x06, exp)
writeIf(write16, base + 0x08, curHp)
writeIf(write16, base + 0x0A, curMp)
writeIf(write16, base + 0x0C, maxHp)
writeIf(write16, base + 0x0E, maxMp)
writeIf(write8, base + 0x10, md)
end
local function cheats()
-- 아군
applyCharacter(0x6000, nil, nil, 9999, nil, nil, nil, nil, nil) -- 나디아
applyCharacter(0x601C, nil, nil, 9999, nil, nil, nil, nil, nil) -- 엘렉트라
applyCharacter(0x6038, nil, nil, 9999, nil, nil, nil, nil, nil) -- 네모
applyCharacter(0x6054, nil, nil, 9999, nil, nil, nil, nil, nil) -- 흰고래
applyCharacter(0x6070, nil, nil, 9999, nil, nil, nil, nil, nil) -- 남극기지
applyCharacter(0x608C, nil, nil, 9999, nil, nil, nil, nil, nil) -- 노틸러스
applyCharacter(0x60A8, nil, nil, 9999, nil, nil, nil, nil, nil) -- 그라탱
applyCharacter(0x60C4, nil, nil, 9999, nil, nil, nil, nil, nil) -- 장
applyCharacter(0x60E0, nil, nil, 9999, nil, nil, nil, nil, nil) -- 킹
applyCharacter(0x60FC, nil, nil, 9999, nil, nil, nil, nil, nil) -- 그란디스
-- 적군
applyCharacter(0x6118, 0x01, 0x01, 0x01, 1, 1, 1, 1, 0x01) -- 황제
applyCharacter(0x6134, 0x01, 0x01, 0x01, 1, 1, 1, 1, 0x01) -- 측근
applyCharacter(0x6150, 0x01, 0x01, 0x01, 1, 1, 1, 1, 0x01) -- 가고일
applyCharacter(0x616C, 0x01, 0x01, 0x01, 1, 1, 1, 1, 0x01) -- 오르세트라스
applyCharacter(0x6188, 0x01, 0x01, 0x01, 1, 1, 1, 1, 0x01) -- 바벨의탑
applyCharacter(0x61A4, 0x01, 0x01, 0x01, 1, 1, 1, 1, 0x01) -- 가피쉬
applyCharacter(0x61C0, 0x01, 0x01, 0x01, 1, 1, 1, 1, 0x01) -- 인간탱크
applyCharacter(0x61DC, 0x01, 0x01, 0x01, 1, 1, 1, 1, 0x01) -- 게로봇
applyCharacter(0x61F8, 0x01, 0x01, 0x01, 1, 1, 1, 1, 0x01) -- 다이소닉스
applyCharacter(0x6214, 0x01, 0x01, 0x01, 1, 1, 1, 1, 0x01) -- 대장
end
emu.addEventCallback(cheats, emu.eventType.endFrame)
IPC 브릿지를 이용한 스탯 모니터링
local socket = require("socket")
local M = emu.memType.nesMemory
local HOST = "127.0.0.1"
local PORT = 5005
local udp = socket.udp()
udp:setpeername(HOST, PORT)
udp:settimeout(0)
local allies = {
{ name = "나디아", base = 0x6000 },
{ name = "엘렉트라", base = 0x601C },
{ name = "네모", base = 0x6038 },
{ name = "흰고래", base = 0x6054 },
{ name = "남극기지", base = 0x6070 },
{ name = "노틸러스", base = 0x608C },
{ name = "그라탱", base = 0x60A8 },
{ name = "장", base = 0x60C4 },
{ name = "킹", base = 0x60E0 },
{ name = "그란디스", base = 0x60FC },
}
local enemies = {
{ name = "황제", base = 0x6118 },
{ name = "측근", base = 0x6134 },
{ name = "가고일", base = 0x6150 },
{ name = "오르세트라스", base = 0x616C },
{ name = "바벨의탑", base = 0x6188 },
{ name = "가피쉬", base = 0x61A4 },
{ name = "인간탱크", base = 0x61C0 },
{ name = "게로봇", base = 0x61DC },
{ name = "다이소닉스", base = 0x61F8 },
{ name = "대장", base = 0x6214 },
}
local function read16(addr)
local lo = emu.read(addr, M)
local hi = emu.read(addr + 1, M)
return lo + hi * 256
end
local function read8(addr)
return emu.read(addr, M)
end
local function buildLine(unit)
local lv = read8(unit.base + 0x03)
local exp = read16(unit.base + 0x06)
local hp = read16(unit.base + 0x08)
local mp = read16(unit.base + 0x0A)
local maxHp = read16(unit.base + 0x0C)
local maxMp = read16(unit.base + 0x0E)
return string.format(
"%s|%d|%d|%d|%d|%d|%d",
unit.name,
lv,
exp,
hp,
maxHp,
mp,
maxMp
)
end
local frame = 0
local function sendStatus()
frame = frame + 1
-- 약 10Hz
if frame % 6 ~= 0 then
return
end
local lines = {}
table.insert(lines, "[ALLY]")
for _, c in ipairs(allies) do
table.insert(lines, buildLine(c))
end
table.insert(lines, "[ENEMY]")
for _, e in ipairs(enemies) do
table.insert(lines, buildLine(e))
end
local packet = table.concat(lines, "\n")
udp:send(packet)
end
emu.addEventCallback(
sendStatus,
emu.eventType.endFrame
)
emu.log("Nadia UDP bridge started")
import socket
import sys
import os
import unicodedata
HOST = "127.0.0.1"
PORT = 5005
if os.name == "nt":
os.system("")
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((HOST, PORT))
def display_width(text):
width = 0
for ch in text:
if unicodedata.east_asian_width(ch) in ("W", "F"):
width += 2
else:
width += 1
return width
def pad_right(text, width):
current = display_width(text)
return text + (" " * max(0, width - current))
def pad_left(text, width):
current = display_width(text)
return (" " * max(0, width - current)) + text
def parse_packet(text):
allies = []
enemies = []
target = None
for raw in text.splitlines():
line = raw.strip()
if not line:
continue
if line == "[ALLY]":
target = allies
continue
if line == "[ENEMY]":
target = enemies
continue
if target is None:
continue
parts = line.split("|")
if len(parts) != 7:
continue
name, lv, exp, hp, max_hp, mp, max_mp = parts
try:
target.append({
"name": name,
"lv": int(lv),
"exp": int(exp),
"hp": int(hp),
"max_hp": int(max_hp),
"mp": int(mp),
"max_mp": int(max_mp),
})
except ValueError:
continue
return allies, enemies
NAME_W = 16
LV_W = 6
EXP_W = 10
HP_W = 16
MP_W = 16
def build_units(units):
lines = []
for u in units:
lv = f"Lv{u['lv']}"
exp = f"EXP {u['exp']}"
hp = f"{u['hp']}/{u['max_hp']}"
mp = f"{u['mp']}/{u['max_mp']}"
line = (
pad_right(u["name"], NAME_W)
+ pad_left(lv, LV_W)
+ pad_left(exp, EXP_W)
+ pad_left(hp, HP_W)
+ pad_left(mp, MP_W)
)
lines.append(line)
return lines
sys.stdout.write("\x1b[2J")
sys.stdout.write("\x1b[?25l")
sys.stdout.flush()
last_screen = ""
try:
while True:
data, _ = sock.recvfrom(65535)
try:
text = data.decode("utf-8")
except UnicodeDecodeError:
continue
allies, enemies = parse_packet(text)
lines = []
lines.extend(build_units(allies))
lines.append("")
lines.extend(build_units(enemies))
screen = "\n".join(lines)
if screen != last_screen:
sys.stdout.write("\x1b[H")
sys.stdout.write(screen)
sys.stdout.write("\x1b[J")
sys.stdout.flush()
last_screen = screen
except KeyboardInterrupt:
pass
finally:
sys.stdout.write("\x1b[?25h")
sys.stdout.write("\n")
sys.stdout.flush()
sock.close()
소스 코드
https://github.com/sunlightface/Fushigi-no-Umi-no-Nadia-KR-
패치
MD5 값을 추출해 검색해도 관련 정보가 잘 나오지 않으므로, 일본판 중 롬 파일명 끝에 (Japan) 또는 (J)만 붙은 파일에 패치를 적용
MD5: 6dc8b6972f6193f450b3a92f7df5e37c (J)패치 파일
MD5: 5607cf2af4d0327bf39adaa664436937 (Japan) 패치 파일