AgentSkillsCN

cpp-developer

按照 CorsixModStudio 代码库的规范编写新的 C++ 代码。当用户提出实现新功能、添加类、创建新文件、编写 C++ 代码,或在 Rainman 库、CDMS GUI,乃至第三方 Lua 层中开发相关功能时,均可使用此技能。触发关键词:实现、添加类、新文件、编写代码、开发、创建、C++、cpp,或“功能”。

SKILL.md
--- frontmatter
name: cpp-developer
description: Write new C++ code that follows CorsixModStudio codebase conventions. Use when the user asks to implement a feature, add a class, create a new file, write C++ code, or develop functionality in the Rainman library, CDMS GUI, or vendored Lua layer. Triggers on requests mentioning "implement", "add class", "new file", "write code", "develop", "create", "C++", "cpp", or "feature".

C++ Developer

Write new C++ code that follows CorsixModStudio codebase conventions across all layers, preferring modern C++20 practices wherever it is safe to do so.

Boy-scout rule: When touching existing code, improve it toward modern C++ if the change is safe and localized (e.g., NULLnullptr, add override, use range-for, add const, replace raw arrays with std::vector). Keep modernization changes within the scope of the files you're already modifying — don't refactor unrelated code.

Workflow

  1. Determine which layer the new code belongs to:

    Rainman (src/rainman/) → Consult references/rainman-patterns.md CDMS (src/cdms/) → Consult references/cdms-patterns.md Vendored Lua (src/rainman/lua502/) → Consult references/lua-rules.md

  2. Apply the common conventions below to all new code.

  3. Default to modern C++20 — see the Modern C++ Practices section. Fall back to legacy style only at existing API boundaries. When modifying existing code, apply boy-scout improvements (add override, nullptr, const, range-for, etc.) within the touched scope.

  4. After writing code, add new files to the appropriate CMakeLists.txt.

  5. Build and verify: cmake --build --preset debug

File Header

Every new .h and .cpp file MUST begin with the LGPL v2.1 copyright block:

cpp
/*
Rainman Library
Copyright (C) 2006 Corsix <corsix@gmail.com>

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

Naming Conventions

ElementConventionExample
Concrete classC prefixCSgaFile, CMemoryStore
Interface/abstract classI prefixIFileStore, IMetaTable
Member variablem_ + Hungarian prefixm_sMessage (string), m_iLine (integer), m_pPrecursor (pointer), m_bInited (bool)
Virtual methodV prefixVInit, VRead, VSeek, VOpenStream
Include guard_UPPER_SNAKE_H__C_SGA_FILE_H_

Hungarian prefixes: s = string/char*, i = integer/unsigned long, p = pointer, b = bool, f = float, ws = wchar_t*, t = table/vector, o = object.

Error Handling

This codebase uses heap-allocated exception objects, not stack-based C++ exceptions. Exceptions are created with new and cleaned up with destroy(), never delete.

Throwing

cpp
// Simple throw
throw new CRainmanException(__FILE__, __LINE__, "message");

// Or use macros:
QUICK_THROW("message");                    // throw new CRainmanException(...)
CATCH_THROW("context message");            // catch + re-throw with precursor

Catching and Cleanup

cpp
try {
    SomeOperation();
} catch (CRainmanException* pE) {
    pE->destroy();  // CRITICAL: always destroy(), never delete
}

// Or use macros:
try { SomeOperation(); } IGNORE_EXCEPTIONS  // catch + destroy
try { SomeOperation(); } CATCH_THROW("Failed during X")  // catch + re-throw

Memory Allocation Checks

cpp
char* pBuffer = CHECK_MEM(new char[1024]);  // throws if allocation fails

Memory Management

New code (preferred)

Use RAII and smart pointers for all new internal code:

cpp
// Prefer std::unique_ptr for owned heap objects
auto pBuffer = std::make_unique<char[]>(1024);

// Use a custom deleter for IStream* returned by VOpenStream()
auto pStream = std::unique_ptr<IFileStore::IStream>(store.VOpenStream("path"));
pStream->VRead(1, sizeof(unsigned long), &iValue);
// stream is automatically deleted when pStream goes out of scope

// Use a custom deleter for CRainmanException* if you need to hold one
auto pEx = std::unique_ptr<CRainmanException, decltype(&CRainmanException::destroy)>(
    pCaught, &CRainmanException::destroy);

Legacy API boundaries

When implementing or overriding existing virtual interfaces that return raw pointers (e.g., VOpenStream(), VOpenOutputStream()), continue to return raw new-allocated objects — callers of those interfaces are responsible for deletion. Within the implementation body, still prefer smart pointers for intermediate allocations.

  • Use CHECK_MEM(new ...) for allocations that must not fail at API boundaries.
  • Use AutoDelete<T> (from Internal_Util.h) when interfacing with legacy cleanup patterns.

RAINMAN_API Macro

All public Rainman classes MUST use the RAINMAN_API macro on their class declaration. It currently expands to nothing (static lib build) but must remain for future DLL compatibility:

cpp
#include "Api.h"

class RAINMAN_API CMyNewClass
{
    // ...
};

Nested public classes also need RAINMAN_API:

cpp
class RAINMAN_API CMyClass : public IFileStore
{
public:
    class RAINMAN_API CStream : public IFileStore::IStream { ... };
};

Modern C++ Practices

Default to modern C++20 in all new code. When editing existing code, apply boy-scout improvements within the scope of your change. Only preserve legacy style at existing API boundaries where changing would cascade.

Smart pointers & RAII

  • Prefer std::unique_ptr / std::make_unique for owned heap objects.
  • Wrap raw-pointer returns from legacy APIs (e.g., VOpenStream()) in std::unique_ptr at the call site for automatic cleanup.
  • For CRainmanException*, use a unique_ptr with a custom deleter that calls destroy().

Modern types (internal / new code)

UseInstead ofWhen
std::string / std::string_viewchar* / const char*Internal storage & parameters
std::size_tunsigned longInternal sizes & loop counters
std::uint32_t, std::int64_tunsigned long, longInternal fixed-width data
std::vector<T>T* + manual new[]/delete[]Owning dynamic arrays
std::array<T,N>T[N] raw arrayFixed-size arrays
std::optional<T>sentinel value / out-parameterNullable return values
std::span<T> (C++20) or pointer+sizeT* + separate lengthNon-owning views (when C++20 is available)

API boundaries: Public virtual interfaces and methods that override existing signatures must continue using unsigned long, char*, etc. to match.

Keywords & syntax

  • auto — use for iterators, factory returns, lambdas, and any type that is obvious from the right-hand side. Avoid auto when the type is not immediately clear.
  • nullptr — never use NULL or 0 for null pointers.
  • override — always mark virtual overrides. Add override to every reimplemented virtual method.
  • const / constexpr — mark variables, parameters, and methods const wherever possible. Use constexpr for compile-time constants.
  • enum class — prefer over plain enum for new enumerations.
  • [[nodiscard]] — add to functions whose return value must not be ignored (e.g., factory functions, error codes).
  • noexcept — mark functions that are guaranteed not to throw (note: the codebase throws CRainmanException*, so many functions cannot be noexcept).

Loops & algorithms

  • Prefer range-based for over index-based iteration:
    cpp
    for (const auto& item : m_tItems) { ... }
    
  • Prefer <algorithm> functions (std::find_if, std::transform, std::any_of, etc.) over hand-written loops when they improve clarity.
  • Use structured bindings for pairs, tuples, and map entries:
    cpp
    for (const auto& [key, value] : myMap) { ... }
    

Strings

  • Prefer std::string for new string storage and std::string_view for non-owning read-only access.
  • Use std::format (C++20, or fmt::format if available) over sprintf / snprintf for new formatting code if the build supports it; otherwise std::ostringstream or snprintf is acceptable.

Initialization

  • Use brace initialization for aggregates and containers:
    cpp
    std::vector<int> ids = {1, 2, 3};
    
  • Prefer in-class member initializers over constructor initializer lists for default values:
    cpp
    class CMyClass {
        bool m_bInited = false;
        unsigned long m_iCount = 0;
    };
    

What NOT to modernize

These legacy patterns must be preserved for compatibility — do not change them:

  • Heap-allocated exceptions (throw new CRainmanException) — this is a project-wide convention, not a mistake.
  • RAINMAN_API macro on public classes.
  • Hungarian-prefixed member names (m_sName, m_pStream).
  • C/I class prefixes and V virtual-method prefix.
  • Include guards (_C_CLASS_NAME_H_ style) — do not replace with #pragma once.
  • unsigned long in public/virtual API signatures — changing cascades.
  • Vendored Lua code — never touch lua502/ or lua512/.

Legacy Type Rules (API boundaries only)

These rules apply when implementing or overriding existing public/virtual interfaces. For purely internal new code, prefer modern types (see above).

  • Use unsigned long for sizes and counts in existing public APIs (changing would cascade through the codebase).
  • Use long for signed offsets in existing seek interfaces.
  • Use Windows-only APIs where the codebase already does (_wfopen, LoadLibraryW, GetProcAddress).
  • Match the existing cast style (C-style or static_cast) of the file you're editing. In new files, prefer static_cast / reinterpret_cast / const_cast.

Build Integration

Rainman (src/rainman/CMakeLists.txt)

Sources are collected via file(GLOB), so new .cpp/.c files are auto-discovered. Just add the file and rebuild.

CDMS (src/cdms/CMakeLists.txt)

Same file(GLOB) pattern — new source files are auto-discovered.

Tests (tests/rainman/CMakeLists.txt)

Test files must be explicitly listed in the add_executable(rainman_tests ...) call:

cmake
add_executable(rainman_tests
    # ... existing files ...
    newfile_test.cpp    # Add new test file here
)

Build & Verify

powershell
cmake --build --preset debug
ctest --preset debug