Skip to content

Latest commit

 

History

History
531 lines (414 loc) · 20.5 KB

File metadata and controls

531 lines (414 loc) · 20.5 KB

CLAUDE.md

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

Project Overview

CommonLibSSE NG is a modern C++ library for SKSE (Skyrim Script Extender) plugin development that supports multiple Skyrim runtimes: SE (Special Edition), AE (Anniversary Edition), and VR. It provides a comprehensive C++ API for interacting with Skyrim's game engine and is designed to enable single-DLL plugins that work across all supported runtimes.

Multi-Runtime Architecture

The library uses a unique multi-targeting system that allows plugins to support SE, AE, and VR simultaneously:

  • Runtime detection happens at loading time
  • Code can probe for runtime-specific features before using them
  • Address Library integration handles different game versions automatically
  • Conditional compilation flags control runtime support: ENABLE_SKYRIM_SE, ENABLE_SKYRIM_AE, ENABLE_SKYRIM_VR

Build System Commands

Quick Build Script (Recommended for AI Agents)

For fast iteration and error detection, use the comprehensive build script:

# From Windows
.\scripts\build-all-presets.cmd

# From WSL/Linux
cmd.exe /c "scripts\build-all-presets.cmd"

This script:

  • Builds all presets (SE, AE, VR, Flatrim, All) in sequence
  • Stops at first failure for fast error detection
  • Shows only errors (filters out verbose output)
  • Perfect for LLM-assisted development iterations

Building from WSL (Claude Code, Linux Environments)

IMPORTANT: This project requires Windows-specific tooling (Visual Studio 2022, MSVC compiler). Claude Code runs in WSL and must use PowerShell to invoke the Windows build tools.

CRITICAL: CMake must run from a Visual Studio Developer Shell to properly detect the MSVC compiler. If run from a regular shell, CMake may incorrectly detect clang++ and fail with compiler errors.

# WSL/Linux: Use powershell.exe to launch VS Developer Shell and build
# IMPORTANT: Adjust the VS installation path to match your system
# The examples below use F:\Program Files but yours may be on C:\ or another drive
# Standard path: <DRIVE>:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1

# Configure and build VR preset (adjust path to your VS installation)
powershell.exe "& '<DRIVE>:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1' -Arch amd64; cmake --preset build-release-msvc-vcpkg-vr; cmake --build build/release-msvc-vcpkg-vr"

# Example with F: drive:
powershell.exe "& 'F:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1' -Arch amd64; cmake --preset build-release-msvc-vcpkg-vr; cmake --build build/release-msvc-vcpkg-vr"

# Configure and build ALL preset (SE + AE + VR)
powershell.exe "& '<DRIVE>:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1' -Arch amd64; cmake --preset build-release-msvc-vcpkg-all; cmake --build build/release-msvc-vcpkg-all"

# Configure and build FLATRIM preset (SE + AE, no VR)
powershell.exe "& '<DRIVE>:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1' -Arch amd64; cmake --preset build-release-msvc-vcpkg-flatrim; cmake --build build/release-msvc-vcpkg-flatrim"

# Run tests (from Windows side after build)
powershell.exe "ctest --preset all"

Building from Native Windows

# Option 1: Launch VS Developer PowerShell manually, then run:
cmake --preset build-release-msvc-vcpkg-all
cmake --build build/release-msvc-vcpkg-all

# Option 2: Use VS Dev Shell launcher in one command (adjust path to your VS installation):
& "<DRIVE>:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64
cmake --preset build-release-msvc-vcpkg-vr
cmake --build build/release-msvc-vcpkg-vr

Package Manager

The project uses Vcpkg for dependency management:

  • Vcpkg: All presets use -vcpkg- in the name
  • Dependencies are defined in vcpkg.json

Runtime-Specific Builds

Available runtime configurations:

  • all: Supports SE, AE, and VR
  • flatrim: SE and AE only (no VR)
  • vr: VR only
  • se: SE only
  • ae: AE only

Test Categories

  • Unit tests: [unit] - No Skyrim module required
  • Integration tests: [integration] - Skyrim module at rest
  • End-to-end tests: [e2e] - Full Skyrim engine required

Code Architecture

Directory Structure

  • include/RE/: Reverse-engineered Skyrim classes organized alphabetically
  • include/REL/: Address Library integration and relocation utilities
  • include/REX/: Cross-platform abstraction layer
  • include/SKSE/: SKSE plugin framework interfaces
  • src/: Implementation files mirroring the include structure
  • tests/: Unit and integration tests using Catch2

Key Components

  • RE namespace: Skyrim engine classes (Actor, TESForm, etc.)
  • REL namespace: Address resolution and version management
  • SKSE namespace: Plugin lifecycle, event systems, and serialization
  • BST containers: Bethesda's STL-like container classes
  • Havok integration: Physics system bindings

Plugin Declaration

Use the simplified CMake function instead of manual plugin setup:

find_package(CommonLibSSE REQUIRED)
add_commonlibsse_plugin(${PROJECT_NAME} SOURCES ${sources})
target_link_libraries(${PROJECT_NAME} PUBLIC CommonLibSSE::CommonLibSSE)

Dependencies and Requirements

Build Dependencies

  • Visual Studio 2022 with C++ Desktop Development
  • CMake 3.19+
  • Vcpkg or Conan
  • Address Library for target runtimes

C++ Standard

The project uses C++23 (cxx_std_23 is set in CMakeLists.txt)

Address Library Integration

Essential for cross-version compatibility:

  • SE: Address Library for SKSE Plugins
  • VR: VR Address Library for SKSEVR
  • Version detection and offset resolution handled automatically

Development Workflow

Making Changes

  1. Understand the multi-runtime implications of your changes
  2. Use runtime feature detection when accessing version-specific functionality
  3. Follow existing patterns for BST containers and Skyrim conventions
  4. Run unit tests before integration tests
  5. Test across different runtime configurations when possible

Testing Strategy

  • Write unit tests for logic that doesn't require Skyrim
  • Use integration tests for Skyrim API interactions
  • Reserve e2e tests for full engine functionality
  • Tests can load different Skyrim executables for cross-runtime validation

Code Organization

  • Reverse-engineered classes follow Skyrim's naming conventions
  • Use RTTI information for class hierarchies and virtual functions
  • Address Library IDs are managed through REL::ID system
  • Cross-runtime code uses feature detection patterns

Multi-Runtime Conditional Patterns

CommonLibSSE NG handles runtime differences using two main conditional patterns:

Pattern 1: Runtime-Exclusive Virtual Functions

Use this pattern when a class has the same base class across runtimes, but different virtual functions exist in different runtimes.

Example: Camera state classes all inherit from TESCameraState, but VR has an extra Unk_03() virtual function that SE/AE don't have.

For runtime-exclusive virtual functions (e.g., VR-only camera state Unk_03()):

Base Class Pattern:

// Header (.h file) - Three-way conditional pattern
#if defined(EXCLUSIVE_SKYRIM_FLAT)
    // Function doesn't exist in SE/AE-only builds
#elif defined(EXCLUSIVE_SKYRIM_VR)
    virtual void Unk_03();  // 03 - VR only
#else
    void Unk_03();          // 03 - Multi-runtime (non-virtual)
#endif

// Implementation (.cpp file)
#ifdef SKYRIM_CROSS_VR
    void ClassName::Unk_03()
    {
        if (REL::Module::IsVR()) {
            REL::RelocateVirtual<decltype(&ClassName::Unk_03)>(0x03, 0x03, this);
        }
        // SE/AE: no-op, this function should never be called
    }
#endif

Derived Class Pattern:

// Header (.h file) - Must match base class pattern
#if defined(EXCLUSIVE_SKYRIM_FLAT)
    // Function doesn't exist in SE/AE-only builds
#elif defined(EXCLUSIVE_SKYRIM_VR)
    void Unk_03() override;  // 03 - VR only
#else
    void Unk_03();           // 03 - Multi-runtime
#endif

// Implementation (.cpp file) - Same as base class
#ifdef SKYRIM_CROSS_VR
    void DerivedClass::Unk_03()
    {
        if (REL::Module::IsVR()) {
            REL::RelocateVirtual<decltype(&DerivedClass::Unk_03)>(0x03, 0x03, this);
        }
        // SE/AE: no-op, this function should never be called
    }
#endif

CRITICAL: Vtable Slot Alignment

When a runtime-exclusive function exists, ALL virtual functions that come after it in the inheritance hierarchy get shifted in the vtable:

// SE/AE vtable: Begin(01) -> End(02) -> Update(03) -> GetRotation(04)
// VR vtable:    Begin(01) -> End(02) -> Unk_03(03) -> Update(04) -> GetRotation(05)

Every derived class that overrides functions after a runtime-exclusive function MUST implement RelocateVirtual for those functions:

#ifdef SKYRIM_CROSS_VR
    void DerivedClass::Update(BSTSmartPointer<TESCameraState>& a_nextState)
    {
        REL::RelocateVirtual<decltype(&DerivedClass::Update)>(0x03, 0x04, this, a_nextState);
        //                                                    SE^   VR^ (shifted by Unk_03)
    }

    void DerivedClass::GetRotation(NiQuaternion& a_rotation)
    {
        REL::RelocateVirtual<decltype(&DerivedClass::GetRotation)>(0x04, 0x05, this, a_rotation);
        //                                                         SE^   VR^ (shifted by Unk_03)
    }
#endif

Build behavior:

  • EXCLUSIVE_SKYRIM_FLAT (SE/AE-only): Function doesn't exist, no vtable shift
  • EXCLUSIVE_SKYRIM_VR (VR-only): Function declared as virtual, engine provides implementation
  • Multi-runtime (ALL/FLATRIM): Function uses RelocateVirtual with runtime detection

Note: This pattern works for any runtime-exclusive function (VR-only, SE-only, AE-only, etc.), though VR-only functions are the most common case.

Do NOT use SKYRIM_REL_VR_VIRTUAL for runtime-exclusive functions - it breaks vtable alignment in multi-runtime builds. Use SKYRIM_REL_VR_VIRTUAL only for functions that exist across all runtimes but may need different calling conventions.

Pattern 2: Runtime-Exclusive Inheritance

Use this pattern when a class has completely different base classes in different runtimes.

Example: ButtonEvent inherits from VRWandEvent in VR but from IDEvent in SE/AE. HUDMenu inherits from WorldSpaceMenu in VR but from IMenu in SE/AE.

Inheritance Pattern:

// Header (.h file) - Three-way conditional inheritance
class ClassName :
#if defined(EXCLUSIVE_SKYRIM_VR)
    public VROnlyBaseClass,
    public SharedBaseClass1,
    public SharedBaseClass2
#elif !defined(ENABLE_SKYRIM_VR)  // SE/AE-only
    public SEAEOnlyBaseClass,
    public SharedBaseClass1,
    public SharedBaseClass2
#else
    // Multi-runtime: can't inherit from incompatible base classes
    public CommonBaseClass  // Choose most compatible base
#endif
{
public:
    // Upcast functions for multi-runtime builds
    [[nodiscard]] VROnlyBaseClass* AsVROnlyBaseClass() noexcept
    {
        if SKYRIM_REL_CONSTEXPR (!REL::Module::IsVR()) {
            return nullptr;
        }
        return &REL::RelocateMember<VROnlyBaseClass>(this, 0, 0);
    }

    [[nodiscard]] SEAEOnlyBaseClass* AsSEAEOnlyBaseClass() noexcept
    {
        if SKYRIM_REL_CONSTEXPR (REL::Module::IsVR()) {
            return nullptr;
        }
        return &REL::RelocateMember<SEAEOnlyBaseClass>(this, 0, 0);
    }
};

Critical: Use !defined(ENABLE_SKYRIM_VR) for SE/AE-only case, not just #else, to ensure proper compilation across all preset types.

Build behavior:

  • EXCLUSIVE_SKYRIM_VR (VR-only): Inherits from VR-specific base classes
  • SE/AE-only builds: Inherits from SE/AE-specific base classes
  • Multi-runtime (ALL): Inherits from most compatible base, provides upcast functions that return nullptr when invalid

Pattern 3: Chained Inheritance Access

Use this pattern when you need to access base class functionality through a chain of inheritance that differs between runtimes.

Example: ButtonEvent needs access to IDEvent members, but:

  • VR: ButtonEventVRWandEventIDEventInputEvent
  • SE/AE: ButtonEventIDEventInputEvent
  • Multi-runtime: ButtonEventInputEvent (no direct inheritance relationship to IDEvent)

Access Function Pattern:

[[nodiscard]] IDEvent* AsIDEvent() noexcept
{
#if defined(EXCLUSIVE_SKYRIM_VR)
    // VR builds: Navigate through VRWandEvent to reach IDEvent
    return static_cast<IDEvent*>(static_cast<VRWandEvent*>(this));
#elif !defined(ENABLE_SKYRIM_VR)
    // SE/AE builds: Direct inheritance from IDEvent
    return static_cast<IDEvent*>(this);
#else
    // Multi-runtime builds: No inheritance relationship, use RelocateMember
    return &REL::RelocateMember<IDEvent>(this, 0, 0);
#endif
}

// Accessor functions that work across all runtimes
[[nodiscard]] std::uint32_t GetIDCode() const noexcept
{
    if (auto idEvent = AsIDEvent()) {
        return idEvent->idCode;
    }
    return 0;  // Fallback for invalid cases
}

void SetIDCode(std::uint32_t a_idCode)
{
    if (auto idEvent = AsIDEvent()) {
        idEvent->idCode = a_idCode;
    }
}

Key Points:

  • Single-runtime builds: Use efficient static_cast following known inheritance chains
  • Multi-runtime builds: Use RelocateMember since inheritance relationship doesn't exist at compile time
  • Always validate: Check return values from upcast functions for safety
  • Consistent API: Accessor functions provide the same interface regardless of runtime

Build behavior:

  • EXCLUSIVE_SKYRIM_VR: Uses double static_cast through inheritance chain
  • SE/AE-only builds: Uses direct static_cast
  • Multi-runtime: Uses RelocateMember for runtime-specific memory layout access

Multi-Runtime Architecture Patterns

CommonLibSSE NG uses different abstraction patterns based on the type and complexity of runtime differences:

Virtual Functions (Explicit Pattern - Recommended):

  • Use three-way conditionals for clarity and maintainability
  • Virtual functions are relatively rare, so explicit patterns don't create significant maintenance burden
  • Clear separation between runtime-exclusive and cross-runtime functions

Data Structures (Macro Pattern - Established):

  • Use RUNTIME_DATA_CONTENT macros for complex structural differences
  • Beneficial when multiple large structs have significant layout differences
  • Examples: HUDMenu, State, BGSShaderParticleGeometryData

Inheritance (Three-way Conditional):

  • Use explicit conditionals for different base class hierarchies
  • Example: VR's WorldSpaceMenu vs SE/AE's IMenu inheritance

Individual Members (Mixed Approach):

  • Simple exclusions: #ifndef SKYRIM_CROSS_VR
  • Type differences: Three-way conditionals
  • Runtime detection: Use REL::Module::IsVR() when both layouts exist

Maintainability Guidelines:

  • Prefer explicit patterns for infrequent changes (virtual functions)
  • Use macro abstraction for repetitive complex patterns (data structures)
  • Document offset calculations and Address Library relationships
  • Standardize member accessor patterns using RelocateMember

CommonLibVR Code Conventions & Best Practices

Virtual Function Implementation Guidelines

CRITICAL: Always verify virtual function signatures match the base class exactly, including all parameters.

Common Mistakes to Avoid:

  1. Incorrect Override Declarations

    • WRONG: Adding override when function doesn't actually override
    • CORRECT: Only use override when signature exactly matches base class
    // Base class: void OnVisible(NiCullingProcess&, std::int32_t)
    
    // WRONG - missing parameter, doesn't actually override
    void OnVisible(NiCullingProcess& a_process) override;
    
    // CORRECT - matches base class signature
    void OnVisible(NiCullingProcess& a_process, std::int32_t a_alphaGroupIndex) override;
  2. Three-Way Conditional Pattern

    • Always use the standard pattern for runtime-exclusive virtual functions:
    #if defined(EXCLUSIVE_SKYRIM_FLAT)
        void MyFunction() override;  // SE/AE only - virtual
    #elif defined(EXCLUSIVE_SKYRIM_VR)
        // VR has different vtable - function doesn't exist
    #else
        void MyFunction();  // Multi-runtime - non-virtual wrapper
    #endif
  3. Static Assert Patterns

    • Use conditional static_assert for classes with three-way conditionals:
    #if defined(EXCLUSIVE_SKYRIM_FLAT)
        static_assert(sizeof(MyClass) == 0x170);
    #endif
    • Use STATIC_ASSERT_SIZE macro only for simple multi-runtime structs without conditional members

Resource Management

  1. REL::Relocation Must Be Static

    • WRONG: REL::Relocation<Type*> singleton{ REL::ID(12345) };
    • CORRECT: static REL::Relocation<Type*> singleton{ REL::ID(12345) };

    Static ensures singleton is initialized once and persists across calls.

  2. Runtime Data Accessor Pattern

    • Classes with RUNTIME_DATA_CONTENT macros must use accessor functions:
    // WRONG - direct member access fails in multi-runtime builds
    this->root.Invoke("method", &result);
    
    // CORRECT - use GetRuntimeData() accessor
    auto runtimeData = GetRuntimeData();
    runtimeData.root.Invoke("method", &result);

File Organization

  1. Empty Implementation Files

    • Delete .cpp files that only contain comments or boilerplate
    • Update cmake/sourcelist.cmake when removing files
    • If methods are inline in header, no .cpp needed
  2. Implementation Files for Multi-Runtime

    • Virtual function wrappers for multi-runtime builds need .cpp files
    • Always guard with #ifdef SKYRIM_CROSS_VR
    • Use REL::RelocateVirtual to dispatch to correct runtime vtable

Comments and Documentation

  1. No Version Difference Comments

    • WRONG: // Moved to OtherFile.h
    • WRONG: float member; // 0x158 in SE/AE, 0x110 in VR
    • CORRECT: float member; // 158 / 110 (standard offset format)
  2. Keep Comments Minimal

    • Only add comments explaining why, not what
    • Virtual function slot numbers are sufficient: void Update() override; // 05
    • Remove TODO comments after issues are resolved

Common Build Errors and Solutions

Error: "method with override specifier 'override' did not override any base class methods"

  • Cause: Function signature doesn't match base class
  • Solution: Check base class signature, ensure all parameters match exactly

Error: "is not a member of [class]"

  • Cause: Trying to access RUNTIME_DATA members directly in multi-runtime builds
  • Solution: Use GetRuntimeData() accessor function

Error: "overloaded member function not found"

  • Cause: Header declaration and implementation parameter count mismatch
  • Solution: Ensure .cpp implementation matches header declaration exactly

Error: "static assertion failed" (size mismatch)

  • Cause: Class size doesn't match expected size for current build configuration
  • Solution:
    1. Verify member offsets and padding
    2. Check if using correct size for SE/AE vs VR vs multi-runtime
    3. Use #if defined(EXCLUSIVE_SKYRIM_FLAT) pattern for conditional assertions

Testing Workflow for AI Agents

  1. Use the Build Script

    .\scripts\build-all-presets.cmd
    • Stops at first error for fast iteration
    • Shows only error messages
    • Tests all runtime configurations
  2. Incremental Testing

    • Fix errors one at a time
    • Run build script after each fix
    • Don't proceed to next error until current one is resolved
  3. Multi-Runtime Verification

    • Changes affecting virtual functions MUST be tested in all presets
    • VR, Flatrim (SE+AE), and All presets can have different compilation results
    • Runtime data access patterns are especially critical in All preset

Quick Reference: Preprocessor Defines

  • ENABLE_SKYRIM_SE - SE runtime support enabled
  • ENABLE_SKYRIM_AE - AE runtime support enabled
  • ENABLE_SKYRIM_VR - VR runtime support enabled
  • EXCLUSIVE_SKYRIM_FLAT - Only SE/AE enabled (no VR)
  • EXCLUSIVE_SKYRIM_VR - Only VR enabled (no SE/AE)
  • SKYRIM_CROSS_VR - Both VR and (SE or AE) enabled (multi-runtime)

Build Preset Combinations:

  • se: Only ENABLE_SKYRIM_SE, defines EXCLUSIVE_SKYRIM_FLAT
  • ae: Only ENABLE_SKYRIM_AE, defines EXCLUSIVE_SKYRIM_FLAT
  • vr: Only ENABLE_SKYRIM_VR, defines EXCLUSIVE_SKYRIM_VR
  • flatrim: ENABLE_SKYRIM_SE + ENABLE_SKYRIM_AE, defines EXCLUSIVE_SKYRIM_FLAT
  • all: All three enabled, defines SKYRIM_CROSS_VR