8.5 KiB
Architecture Guide
Design Principles
- C++14 minimum — No C++17 features. This enables compilation with older toolchains including Open Watcom for Win9x targets.
- No mandatory external dependencies — cJSON is vendored. HTTP backends are optional link targets.
- Backend abstraction — HTTP, archive extraction, and script execution are all behind abstract interfaces. Consumers provide platform-specific implementations.
- Value types — Models are plain structs. No inheritance hierarchies, no virtual methods, no allocator magic. They copy and move naturally.
- Synchronous API — All methods are blocking. Async behavior is the caller's responsibility (threads, event loops, etc.). This avoids platform-specific async primitives.
Library Layers
┌─────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────┤
│ Clients (GameClient, etc.) │
│ ConnectionClient (stateful) │
│ Script Execution │
│ Archive Extraction │
├─────────────────────────────────────────────┤
│ JSON Parsing (internal) │
├─────────────────────────────────────────────┤
│ IHttpClient (abstract) │
├──────────────────┬──────────────────────────┤
│ WinInetHttpClient │ CurlHttpClient │
│ (Windows 95+) │ (any platform) │
└──────────────────┴──────────────────────────┘
Core Library (lancommander)
The static library contains:
- All client implementations
- JSON parsing (cJSON + helpers)
- CRC32 utility
- BatchScriptRunner
It does not contain any HTTP backend. You must link one separately.
HTTP Backends
Each backend is a separate static library:
lancommander_wininet— Uses the WinINet API (wininet.dll). Available on every Windows version since 95 with Internet Explorer installed. No external downloads needed.lancommander_curl— Uses libcurl. Works everywhere curl works. Only built if CMake finds curl on the system.
JSON Layer
JSON parsing uses cJSON, a single-file C library vendored in vendor/cjson/. The parsing helpers in src/json/ are internal to the library and not part of the public API.
The helpers support dual-case field lookup (camelCase and PascalCase) because the LANCommander server may serialize fields in either convention depending on the endpoint.
Extending the Library
Adding a New HTTP Backend
- Create a class that inherits from
IHttpClient - Implement all virtual methods
- Optionally add it to CMakeLists.txt as a separate static library target
#include <lancommander/http/http_client.h>
class MyHttpClient : public lancommander::IHttpClient {
public:
void set_base_url(const std::string& url) override { /* ... */ }
void set_bearer_token(const std::string& token) override { /* ... */ }
HttpResponse get(const std::string& path) override { /* ... */ }
HttpResponse post(const std::string& path, const std::string& body,
const std::string& content_type) override { /* ... */ }
HttpResponse put(const std::string& path, const std::string& body,
const std::string& content_type) override { /* ... */ }
HttpResponse del(const std::string& path) override { /* ... */ }
bool download(const std::string& path, const std::string& dest_path,
DownloadProgressFn progress) override { /* ... */ }
HttpResponse post_multipart_file(const std::string& path,
const std::string& field_name,
const std::string& file_path) override { /* ... */ }
};
Adding a New Archive Extractor
Implement IArchiveExtractor. A minizip-based example:
#include <lancommander/archive/archive_extractor.h>
#include <lancommander/archive/crc32_util.h>
class MinizipExtractor : public lancommander::IArchiveExtractor {
public:
lancommander::ExtractionResult extract(
const std::string& archive_path,
const std::string& dest_directory,
bool skip_existing_matching_crc,
lancommander::ExtractionProgressFn progress) override
{
// 1. Open the zip with minizip's unzOpen
// 2. Iterate entries with unzGoToNextFile
// 3. For each entry:
// a. If skip_existing_matching_crc, compute CRC32 of the local file
// using Crc32::file_crc32() and compare to the entry's CRC
// b. If CRCs match, skip extraction
// c. Otherwise, extract to dest_directory
// 4. Call progress callback periodically
// 5. Return ExtractionResult
}
std::vector<lancommander::ArchiveEntry> list(
const std::string& archive_path) override
{
// Open and enumerate without extracting
}
};
Adding a New Script Runner
Implement IScriptRunner for your platform:
#include <lancommander/script/script_runner.h>
class ShellScriptRunner : public lancommander::IScriptRunner {
public:
lancommander::ScriptResult run_file(
const std::string& script_path,
const std::string& working_directory,
const std::map<std::string, std::string>& variables) override
{
// 1. Fork/exec /bin/sh with the script
// 2. Set environment variables from the map
// 3. Capture stdout/stderr
// 4. Return ScriptResult with exit code
}
lancommander::ScriptResult run_inline(
const std::string& script_contents,
const std::string& working_directory,
const std::map<std::string, std::string>& variables) override
{
// Write to temp file, run_file(), delete temp file
}
};
Adding a New API Client
Follow the existing pattern:
- Model header in
include/lancommander/models/— plain struct - Client header in
include/lancommander/clients/— class withIHttpClient&member - Client implementation in
src/clients/— usesjson::JsonDocandjson::parse_*helpers - JSON parser in
src/json/json_helpers.h/.cpp— addparse_your_model()function - CMakeLists.txt — add the
.cppto the source list - Umbrella header — add includes to
lancommander.h
Template:
// include/lancommander/clients/foo_client.h
class FooClient {
public:
explicit FooClient(IHttpClient& http);
Result<Foo> get(const std::string& id);
private:
IHttpClient& m_http;
};
// src/clients/foo_client.cpp
#include "lancommander/clients/foo_client.h"
#include "../json/json_helpers.h"
#include <sstream>
namespace lancommander {
FooClient::FooClient(IHttpClient& http) : m_http(http) {}
Result<Foo> FooClient::get(const std::string& id)
{
HttpResponse resp = m_http.get("/api/Foos/" + id);
if (!resp.ok()) {
std::ostringstream e;
e << "GetFoo failed (HTTP " << resp.status_code << ")";
return Result<Foo>::fail(e.str());
}
json::JsonDoc doc(resp.body);
if (!doc) return Result<Foo>::fail("Invalid JSON response");
Foo f = json::parse_foo(doc.root);
return Result<Foo>::ok(std::move(f));
}
} // namespace lancommander
Error Handling
The library does not use exceptions. All fallible operations return Result<T>. This is intentional for:
- Compatibility with compilers/runtimes that don't support exceptions (e.g. some Win9x toolchains)
- Predictable control flow
- Lightweight builds with
-fno-exceptions
Internal errors (JSON parse failures, HTTP errors) are captured in Result::error as human-readable strings that include the HTTP status code where applicable.
Thread Safety
The library is not thread-safe by default. Each IHttpClient instance and each client should be used from a single thread. If you need concurrent access, either:
- Create separate
IHttpClient+ client instances per thread - Add your own synchronization around client calls