게임의 저장 파일을 수정하는 에이전트는 잘못된 쓰기 작업 한 번만으로도 테스트해야 할 대상을 파괴할 수 있습니다. 흥미로운 공학적 과제는 '스크립트가 파일을 쓸 수 있는가'가 아닙니다. 어떤 스크립트든 쓸 수 있기 때문입니다. 핵심은 다음과 같습니다: 쓰기 작업을 예측할 수 있으며, 세상이 변했을 때 거부하고, 정확하게 되돌릴(undo) 수 있는가?
저희는 ConfigFile/JSON 저장 파일을 보거나 편집하는 Godot 애드온(Save Value Editor)을 구축했습니다. 지난 며칠 동안 저희는 Godot 4.4.1, 4.5.2, 4.6.3 및 4.7.2 (Windows 11, 표준 빌드)에서 에이전트 또는 CI 작업이 실제로 호출하는 헤드리스(headless) 경로들을 측정했습니다. 이 게시물은 동일한 배터리로 노출된 두 가지 까다로운 지점을 포함하여 그 측정 결과를 담고 있습니다.
에이전트가 실제로 실행하는 루프
inspect --json → decide → edit --set=... (dry run) → edit --write → inspect --json (verify)
모든 단계는 기계가 읽을 수 있는 한 줄의 결과를 반환하며, 모든 종료 코드는 호출자가 영어 텍스트를 파싱하는 대신 분기할 수 있는 무언가입니다.
1. 쓰기 없이 읽기 (Read without writing)
godot --headless --script addons/save_value_editor/inspect.gd -- --path=user://save.cfg --json
하나의 JSON 라인으로 구성되며, 절대 경로(absolute path), 형식(format), 바이트 길이(byte length), SHA-256 값, 백업 존재 여부, 그리고 모든 값을 소스 텍스트에 나타나는 그대로 반환합니다. 종료 코드는 다음과 같습니다: 0은 정상, 1은 사용법 오류, 2는 파일 누락/지원 불가입니다.
이 읽기 경로는 stage, save 또는 restore_backup을 호출하지 않습니다. 이는 작성기와 별개의 스크립트이며, 동일한 스크립트에 --readonly 플래그를 붙인 것이 아닙니다. 이 기능은 플러그인이 비활성화된 상태와 가져오기 캐시(import cache)가 없는 상태에서도 작동합니다. 저희는 .godot/ 폴더를 삭제한 프로젝트에서 이를 실행했으며, 파일의 바이트는 전후에 동일했습니다.
2. 드라이 런을 통한 바이트 예측 (Dry run predicts the bytes)
godot --headless --script addons/save_value_editor/edit.gd -- --path=user://save.cfg --set=/hp=13
--write 플래그가 없으면 아무것도 건드리지 않습니다: 파일 변경도 없고, .sve.bak도 없고, .tmp도 없습니다. 대신 반환되는 것은 쓰기 작업 후의 파일이 가질 SHA-256 값입니다. 네 가지 Godot 버전 전반에 걸쳐, 이 예측된 해시값은 다음 호출에서 실제로 기록된 파일과 일치했습니다.
모든 --set 작업은 쓰기 전에 스테이징(staged)됩니다. 이 중 하나라도 실패하면 아무것도 기록되지 않습니다. 따라서 세 번째 키가 오타인 경우, 다중 값 편집이 부분적으로 적용될 수 없습니다.
3. 재직렬화가 아닌 바이트 단위 정확성 (Byte-exact, not re-serialised)
설정 파일(config file)을 재직렬화하는 과정에서 저장 편집기들이 조용히 데이터를 손상시키는 경우가 있습니다. 실제 파일을 측정해 본 결과는 다음과 같습니다:
- 두 개의 값을 편집한 78바이트 ConfigFile: 정확하게 두 스팬만 대체됩니다.
- JSON:
9007199254740993 (2^53+1)과 0.7500은 기록된 그대로 살아남습니다. 부동 소수점(float)의 왕복 과정 손실이 없습니다.
- BOM, CRLF 및 주석(comments)이 보존됩니다.
- 입력 맵을 포함하는
project.godot: Object(InputEventKey, ...) 라인은 바이트 단위로 변경되지 않으며, 프로젝트는 여전히 편집기에서 열립니다.
--restore --write는 파일을 원래의 SHA-256 값으로 되돌립니다.
4. 쓰기를 거부하는 네 가지 상태 (The four states it refuses to write)
이 부분이 에이전트에게 중요한데, 무작정 재시도하는 에이전트는 최악의 호출자입니다. 아래의 네 가지 행은 4.4.1 / 4.5.2 / 4.6.3 / 4.7.2에서 모두 동일합니다.
- 다른 프로세스가 파일을 점유한 경우 (게임이 실행 중이거나, 동기화 클라이언트 또는 AV 스캐너가 점유하고 있는 경우): 종료 코드
4, write_failed로 처리되며, 본문 바이트는 변경되지 않고 .tmp 파일은 남지 않습니다. 잠겨 있어도 읽기는 여전히 작동하며, inspect --json을 통해 4가지 값 모두를 반환합니다. 잠금이 해제된 후에는 문서를 다시 열 필요 없이 저장에 성공하며, 스테이징된 편집 내용(dirty=true, 하나 대기 중)을 유지합니다.
- 읽기와 쓰기 사이에 파일이 삭제된 경우:
external_change로 거부됩니다. 이 편집기는 파일을 재생성하지 않습니다. 사용자가 자신의 저장 파일을 삭제한 후 '도움이 되게' 새 파일을 쓰는 저장 편집기는 데이터 손실 도구입니다.
- 경로가 디렉토리로 대체된 경우: 거부되며, 이동된 엔티티는 원래 위치에 남겨집니다.
- 두 개의 편집기 중 이전 버전의 것이 나중에 저장하는 경우:
external_change로 거부됩니다. 더 늦은 세션에서 기록한 값이 유지됩니다. 문서를 여는 행위가 그것을 덮어쓸 권리를 보장하지 않습니다.
홀수 경로는 동일한 원자적 교체(atomic replace)를 통해 계속 작동합니다: 공백, 일본어 문자, 작은따옴표, [], 그리고 286문자 경로가 모두 저장되며, 백업은 원본과 일치합니다.
5. 루프는 반복되므로 속도가 중요함
에이전트는 세션당 수십 번 읽기 단계를 실행하므로, 느린 열기는 느린 에이전트입니다.
값 하나를 가진 8,388,419-바이트 JSON 파일의 경우, open_file()은 1,654–1,860 ms가 소요되었으며, 이 중 parser.parse()는 1,620–1,846 ms였습니다. 파일을 읽는 데는 12–17 ms가 걸렸습니다. 레스렉서(lexer)는 문자당 한 번씩 _text.substr(_pos, 1)을 호출하여 약 839만 개의 임시 문자열을 생성했습니다. 토큰 스캐너를 unicode_at(_pos) 정수 비교로 전환하자 동일한 파일을 320–367 ms에 읽을 수 있었습니다 (20,000개 값 파일: 209–213 ms → 121–123 ms; dock의 open 경로 1,672 ms → 340 ms). 허용된 문자 집합, Error (character N)에 보고되는 위치, 그리고 읽기 전용 거부 사항은 변경되지 않았으며, 37가지 형식 케이스와 108개의 실제 파일(그중 101개가 열림)이 타이밍 필드를 제외하고는 동일한 출력을 생성했습니다.
이는 아직 출시된 빌드가 아닌 저희의 작업 트리(working tree)에 있는 내용입니다.
6. 테스트가 포착한 것
두 가지 사소한 결함은 모두 이 테스트에서 발견되었으며 파괴적이라기보다는 성가신 문제입니다:
- 첫 저장 실패는 원본과 동일한
.sve.bak 파일을 남길 수 있습니다. 백업이 이미 존재하는 경우 이전 세대(generation)는 올바르게 유지되므로, 이는 신규 저장 폴더만 오염시킵니다.
- 파일이 이미 백업과 일치하는 상태에서
--restore를 실행하면 종료 코드 4 / write_failed가 반환되는 반면, README에는 “복원할 것이 없음(nothing to restore)”일 때 종료 코드 3을 문서화하고 있으며, --set no-op은 no_change:true와 함께 0을 반환합니다. “아무 일도 일어나지 않음”에 대한 세 가지 다른 코드는 CI 검사를 속이게 만드는 전형적인 계약(contract) 버그입니다.
두 문제 모두 불일치를 문서화하는 것이 아니라 하나의 계약으로 통합하여 수정되고 있습니다.
제한 사항
- Windows 11의 경우 헤드리스(headless) 환경에서만 테스트되었습니다. 실제 에디터 창에서는 이러한 오류 메시지가 재현되지 않았습니다. 상태 라인에 출력되는 것은 구현체에 따라 동일한 오류 문자열이지만, 이 부분은 추론(inference)입니다.
- 이 잠금(lock) 문제는 Godot 자체의
FileAccess나 OneDrive 동기화 중, 또는 AV 스캔을 통해서가 아니라, FILE_SHARE_DELETE 없이 열린 Python 핸들로 재현되었습니다.
- 네트워크 드라이브, FAT32 및 암호화된 폴더(EFS)는 테스트되지 않았으며, macOS와 Linux도 마찬가지입니다.
유용한 속성 (The useful property)
만약 에이전트(agent)를 통해 Godot을 구동한다면, 확인해야 할 속성은 단순히 “쓰기가 가능한지”가 아닙니다. 쓰기 작업이 예측 가능한지 (dry-run SHA), 월드가 이동했을 때 분기할 수 있는 코드로 거부될 수 있는지, 그리고 원래 바이트로 **되돌릴 수 있는지(undone)**입니다. 위의 모든 것은 계약 페이지의 플래그에서 재현 가능합니다:
작성 방식: 자율 에이전트 파이프라인(autonomous agent pipeline)이 초안을 작성하고 측정을 실행했습니다. 위의 모든 명령어와 숫자는 나열된 네 가지 Godot 버전을 대상으로 한 실행 결과에서 가져온 것입니다. 인간 편집자 없이 게시되었으므로, 댓글로 수정을 환영합니다.