Skip to content

Commit 5ba5cc9

Browse files
committed
RenPy: Support RPA-2.0 (closes #18)
1 parent 5da1751 commit 5ba5cc9

11 files changed

Lines changed: 160 additions & 6 deletions

File tree

CLAUDE.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build Commands
6+
7+
**IMPORTANT: This project ONLY builds on Windows with MSVC toolchain. Do not attempt to build on Linux.**
8+
9+
This project uses CMake with vcpkg for dependency management and requires Visual Studio 2017+ on Windows:
10+
11+
```bash
12+
# Configure build (requires VCPKG_ROOT environment variable)
13+
cmake --preset x64-release # or x86-release
14+
15+
# Build all modules
16+
cmake --build build/x64-release
17+
18+
# Run tests
19+
cd build/x64-release && ctest
20+
21+
# Package for distribution
22+
cd build/x64-release && cpack --config CPackConfig.cmake -C RelWithDebInfo
23+
```
24+
25+
Build presets available: `x64-release`, `x64-debug`, `x86-release`, `x86-debug`
26+
27+
## Development Guidelines
28+
29+
This project uses **C++23** and follows KISS (Keep It Simple, Stupid) and DRY (Don't Repeat Yourself) principles.
30+
31+
**Avoid magic numbers** - use named constants instead.
32+
33+
## Architecture Overview
34+
35+
This project implements Observer plugin modules for FAR Manager that handle exotic archive formats. The codebase follows a plugin architecture where each module implements the Observer API to support different archive formats.
36+
37+
### Core Components
38+
39+
- **API Layer** (`src/api.h`, `src/dll.cpp`): Implements the Observer plugin API with standard functions like `OpenStorage`, `CloseStorage`, `GetItem`, `ExtractItem`
40+
- **Archive Wrapper** (`src/archive.h`, `src/archive.cpp`): Provides a unified interface that wraps format-specific extractors
41+
- **Extractor Interface** (`src/modules/extractor.h`): Defines the abstract interface that all format extractors must implement
42+
43+
### Module Structure
44+
45+
Each supported format has its own module under `src/modules/`:
46+
- `renpy/`: RenPy visual novel archives (.rpa files) with pickle support
47+
- `zanzarah/`: Zanzarah game archives (.pak files)
48+
- `rpgmaker/`: RPG Maker archives (in development)
49+
50+
Each module contains:
51+
- Format-specific implementation (e.g., `renpy.cpp`)
52+
- Module definition file (`.def`) for DLL exports
53+
- Configuration file (`observer_user.ini`)
54+
55+
### Data Flow
56+
57+
1. FAR Manager loads the module DLL via `LoadSubModule()`
58+
2. `OpenStorage()` creates an archive wrapper with format-specific extractor
59+
3. `PrepareFiles()` scans and indexes archive contents
60+
4. `GetItem()` provides file metadata for FAR's file browser
61+
5. `ExtractItem()` handles actual file extraction with progress callbacks
62+
63+
### Testing Framework
64+
65+
Located in `src/tests/` with a custom framework (`framework/observer.h`) that simulates the Observer API for testing archive operations without requiring FAR Manager.

src/archive.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ namespace archive
3434
throw std::runtime_error("Failed to open file");
3535
}
3636
stream_->exceptions(std::ifstream::failbit | std::ifstream::badbit);
37-
stream_->seekg(std::ssize(signature));
3837

3938
return extractor_->get_archive_info(data);
4039
}

src/modules/renpy/renpy.cpp

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "pickle.h"
33

44
#include <fstream>
5+
#include <functional>
56

67
#include <zstr.hpp>
78

@@ -17,7 +18,7 @@ namespace extractor
1718

1819
std::vector<std::byte> extractor::get_signature() noexcept
1920
{
20-
const std::string str = "RPA-3.0 ";
21+
const std::string str = "RPA-";
2122
std::vector<std::byte> signature(str.size());
2223
std::memcpy(signature.data(), str.data(), str.size());
2324
return signature;
@@ -43,11 +44,43 @@ namespace extractor
4344
return result;
4445
}
4546

47+
std::pair<int64_t, std::function<std::pair<int64_t, int64_t>(int64_t, int64_t)> > parse_header(
48+
std::ifstream &stream)
49+
{
50+
std::string version_check(3, '\0');
51+
stream.seekg(static_cast<std::streamoff>(extractor::get_signature().size()));
52+
stream.read(version_check.data(), 3);
53+
54+
if (version_check == "2.0") {
55+
stream.seekg(static_cast<std::streamoff>(std::string("RPA-2.0 ").length()));
56+
const auto index_offset = read_int64(stream);
57+
return {
58+
index_offset, [](int64_t offset, int64_t length)
59+
{
60+
return std::make_pair(offset, length);
61+
}
62+
};
63+
}
64+
65+
if (version_check == "3.0") {
66+
stream.seekg(static_cast<std::streamoff>(std::string("RPA-3.0 ").length()));
67+
const auto index_offset = read_int64(stream);
68+
const auto encryption_key = read_int64(stream);
69+
return {
70+
index_offset, [encryption_key](int64_t offset, int64_t length)
71+
{
72+
return std::make_pair(offset ^ encryption_key, length ^ encryption_key);
73+
}
74+
};
75+
}
76+
77+
throw std::runtime_error("Unsupported RPA version");
78+
}
79+
4680
// ReSharper disable once CppMemberFunctionMayBeStatic
4781
std::vector<std::unique_ptr<file> > extractor::list_files(std::ifstream &stream) // NOLINT(*-convert-member-functions-to-static)
4882
{
49-
const auto index_offset = read_int64(stream);
50-
const auto encryption_key = read_int64(stream);
83+
const auto [index_offset, decoder] = parse_header(stream);
5184

5285
stream.seekg(index_offset);
5386

@@ -75,8 +108,8 @@ namespace extractor
75108
throw std::logic_error("Expected at least 2 elements in tuple");
76109
}
77110

78-
const auto offset = props[0]->as_int64() ^ encryption_key;
79-
const auto body_size = props[1]->as_int64() ^ encryption_key;
111+
const auto [offset, body_size] = decoder(props[0]->as_int64(), props[1]->as_int64());
112+
80113
auto header = std::string();
81114
if (props.size() >= 3) {
82115
if (props[2]->get_type() != pickle::value::type::none) {

src/modules/rpgmaker/rpgmaker.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ namespace extractor
4343
// ReSharper disable once CppMemberFunctionMayBeStatic
4444
std::vector<std::unique_ptr<file> > extractor::list_files(std::ifstream &stream) // NOLINT(*-convert-member-functions-to-static)
4545
{
46+
stream.seekg(static_cast<std::streamoff>(get_signature().size()));
47+
4648
std::vector<std::unique_ptr<file> > files;
4749
const uint32_t magic = read_u32(stream) * 9 + 3;
4850
while (true) {

src/modules/zanzarah/zanzarah.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ namespace extractor
5353
// ReSharper disable once CppMemberFunctionMayBeStatic
5454
std::vector<std::unique_ptr<file> > extractor::list_files(std::ifstream &stream) // NOLINT(*-convert-member-functions-to-static)
5555
{
56+
stream.seekg(static_cast<std::streamoff>(get_signature().size()));
57+
5658
auto files = std::vector<std::unique_ptr<file> >();
5759
files.reserve(read_positive_int32(stream));
5860

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/.venv
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[project]
2+
name = "observer"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.13"
7+
dependencies = [
8+
"xxhash>=3.5.0",
9+
]

src/tests/framework/tools/uv.lock

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)