Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build Commands

**IMPORTANT: This project ONLY builds on Windows with MSVC toolchain. Do not attempt to build on Linux.**

This project uses CMake with vcpkg for dependency management and requires Visual Studio 2017+ on Windows:

```bash
# Configure build (requires VCPKG_ROOT environment variable)
cmake --preset x64-release # or x86-release

# Build all modules
cmake --build build/x64-release

# Run tests
cd build/x64-release && ctest

# Package for distribution
cd build/x64-release && cpack --config CPackConfig.cmake -C RelWithDebInfo
```

Build presets available: `x64-release`, `x64-debug`, `x86-release`, `x86-debug`

## Development Guidelines

This project uses **C++23** and follows KISS (Keep It Simple, Stupid) and DRY (Don't Repeat Yourself) principles.

**Avoid magic numbers** - use named constants instead.

## Architecture Overview

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.

### Core Components

- **API Layer** (`src/api.h`, `src/dll.cpp`): Implements the Observer plugin API with standard functions like `OpenStorage`, `CloseStorage`, `GetItem`, `ExtractItem`
- **Archive Wrapper** (`src/archive.h`, `src/archive.cpp`): Provides a unified interface that wraps format-specific extractors
- **Extractor Interface** (`src/modules/extractor.h`): Defines the abstract interface that all format extractors must implement

### Module Structure

Each supported format has its own module under `src/modules/`:
- `renpy/`: RenPy visual novel archives (.rpa files) with pickle support
- `zanzarah/`: Zanzarah game archives (.pak files)
- `rpgmaker/`: RPG Maker archives (in development)

Each module contains:
- Format-specific implementation (e.g., `renpy.cpp`)
- Module definition file (`.def`) for DLL exports
- Configuration file (`observer_user.ini`)

### Data Flow

1. FAR Manager loads the module DLL via `LoadSubModule()`
2. `OpenStorage()` creates an archive wrapper with format-specific extractor
3. `PrepareFiles()` scans and indexes archive contents
4. `GetItem()` provides file metadata for FAR's file browser
5. `ExtractItem()` handles actual file extraction with progress callbacks

### Testing Framework

Located in `src/tests/` with a custom framework (`framework/observer.h`) that simulates the Observer API for testing archive operations without requiring FAR Manager.
1 change: 0 additions & 1 deletion src/archive.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ namespace archive
throw std::runtime_error("Failed to open file");
}
stream_->exceptions(std::ifstream::failbit | std::ifstream::badbit);
stream_->seekg(std::ssize(signature));

return extractor_->get_archive_info(data);
}
Expand Down
43 changes: 38 additions & 5 deletions src/modules/renpy/renpy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "pickle.h"

#include <fstream>
#include <functional>

#include <zstr.hpp>

Expand All @@ -17,7 +18,7 @@ namespace extractor

std::vector<std::byte> extractor::get_signature() noexcept
{
const std::string str = "RPA-3.0 ";
const std::string str = "RPA-";
std::vector<std::byte> signature(str.size());
std::memcpy(signature.data(), str.data(), str.size());
return signature;
Expand All @@ -43,11 +44,43 @@ namespace extractor
return result;
}

std::pair<int64_t, std::function<std::pair<int64_t, int64_t>(int64_t, int64_t)> > parse_header(
std::ifstream &stream)
{
std::string version_check(3, '\0');
stream.seekg(static_cast<std::streamoff>(extractor::get_signature().size()));
stream.read(version_check.data(), 3);

if (version_check == "2.0") {
stream.seekg(static_cast<std::streamoff>(std::string("RPA-2.0 ").length()));
const auto index_offset = read_int64(stream);
return {
index_offset, [](int64_t offset, int64_t length)
{
return std::make_pair(offset, length);
}
};
}

if (version_check == "3.0") {
stream.seekg(static_cast<std::streamoff>(std::string("RPA-3.0 ").length()));
const auto index_offset = read_int64(stream);
const auto encryption_key = read_int64(stream);
return {
index_offset, [encryption_key](int64_t offset, int64_t length)
{
return std::make_pair(offset ^ encryption_key, length ^ encryption_key);
}
};
}

throw std::runtime_error("Unsupported RPA version");
}

// ReSharper disable once CppMemberFunctionMayBeStatic
std::vector<std::unique_ptr<file> > extractor::list_files(std::ifstream &stream) // NOLINT(*-convert-member-functions-to-static)
{
const auto index_offset = read_int64(stream);
const auto encryption_key = read_int64(stream);
const auto [index_offset, decoder] = parse_header(stream);

stream.seekg(index_offset);

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

const auto offset = props[0]->as_int64() ^ encryption_key;
const auto body_size = props[1]->as_int64() ^ encryption_key;
const auto [offset, body_size] = decoder(props[0]->as_int64(), props[1]->as_int64());

auto header = std::string();
if (props.size() >= 3) {
if (props[2]->get_type() != pickle::value::type::none) {
Expand Down
2 changes: 2 additions & 0 deletions src/modules/rpgmaker/rpgmaker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ namespace extractor
// ReSharper disable once CppMemberFunctionMayBeStatic
std::vector<std::unique_ptr<file> > extractor::list_files(std::ifstream &stream) // NOLINT(*-convert-member-functions-to-static)
{
stream.seekg(static_cast<std::streamoff>(get_signature().size()));

std::vector<std::unique_ptr<file> > files;
const uint32_t magic = read_u32(stream) * 9 + 3;
while (true) {
Expand Down
2 changes: 2 additions & 0 deletions src/modules/zanzarah/zanzarah.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ namespace extractor
// ReSharper disable once CppMemberFunctionMayBeStatic
std::vector<std::unique_ptr<file> > extractor::list_files(std::ifstream &stream) // NOLINT(*-convert-member-functions-to-static)
{
stream.seekg(static_cast<std::streamoff>(get_signature().size()));

auto files = std::vector<std::unique_ptr<file> >();
files.reserve(read_positive_int32(stream));

Expand Down
1 change: 1 addition & 0 deletions src/tests/framework/tools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/.venv
1 change: 1 addition & 0 deletions src/tests/framework/tools/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
9 changes: 9 additions & 0 deletions src/tests/framework/tools/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
name = "observer"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"xxhash>=3.5.0",
]
37 changes: 37 additions & 0 deletions src/tests/framework/tools/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/tests/renpy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

using namespace test;

TEST_CASE("renpy: rpa20_binary_hearts")
{
test_on("renpy\\rpa20_binary_hearts.rpa");
}

TEST_CASE("renpy: rpa30_army_gals")
{
test_on("renpy\\rpa30_army_gals.rpa");
Expand Down