Giao thức CLV1 đã công khaiĐọc đặc tả
Mục lục thư viện

Thư viện tài liệuBắt đầu theo ngôn ngữ

Bắt đầu với Unity

Unity không nạp được SDK C# (SDK đó cần .NET 8), nên game Unity gọi thư viện C của ClavisAUH qua P/Invoke, đúng cách Unity dùng mọi plugin native. Cùng mã đó chạy được trên .NET Framework 4.x. Bài này đi từ cài thư viện tới bản build của game.

5 phút đọc

Cần có

  • Unity với scripting backend Mono hoặc IL2CPP, game build cho Windows x64 hoặc Linux x64. Thư viện C chưa có bản macOS, Android, iOS.
  • Một ứng dụng trên dashboard. Trang Tích hợp › Kết nối phần mềm của nó có địa chỉ API, App ID, khoá công khai và nút Tạo key thử nghiệm. Địa chỉ API kết thúc bằng /api, không có /v1: https://<trang ClavisAUH của bạn>/api.

1. Cài thư viện C

Tải ở trang Tải về và kiểm SHA-256:

  • Windows: clavis-c-1.0.0-windows-x64.zip, gồm clavis.dll: một file, chỉ cần DLL có sẵn của Windows.
  • Linux: clavis-c-1.0.0-linux-x64.tar.gz, gồm libclavis.so.1. Máy chạy game cần libsodium và libcurl của hệ thống (Debian, Ubuntu: apt install libsodium23 libcurl4).

Đặt clavis.dll vào Assets/Plugins/x86_64/ của dự án. Với Linux, chép libclavis.so.1 vào cùng thư mục và đổi tên thành libclavis.so. Trong Inspector của từng file, chọn đúng nền tảng (Windows x86_64 hoặc Linux x86_64) cho cả Editor và Standalone.

2. Mã tối thiểu

Tạo Assets/Scripts/Clavis.cs với lớp dưới đây, nguyên văn. Lớp chỉ dùng DllImport và mảng byte cho chuỗi UTF-8, nên Mono và IL2CPP đều nhận như nhau:

static class Clavis
{
    const string Lib = "clavis";
    const CallingConvention Cdecl = CallingConvention.Cdecl;

    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_open(
        byte[] baseUrl, byte[] appId, byte[] appKey, byte[] stateDir, byte[] appVersion);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern void clavis_close(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_init(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_activate(IntPtr client, byte[] key);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_heartbeat(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_logout(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_offline(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_variable(IntPtr client, byte[] name);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_error_code(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_error_message(IntPtr client, byte[] lang);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_retryable(int error);

    /// <summary>UTF-8 with its closing zero, as every clavis_* string argument.</summary>
    public static byte[] Utf8(string s) => Encoding.UTF8.GetBytes(s + "\0");

    /// <summary>A string the library returns: UTF-8, owned by the client until its next call. Read by hand, as
    /// Marshal.PtrToStringUTF8 is missing from .NET Framework and older Unity.</summary>
    public static string Text(IntPtr p)
    {
        if (p == IntPtr.Zero) return "";
        int length = 0;
        while (Marshal.ReadByte(p, length) != 0) length++;
        var bytes = new byte[length];
        Marshal.Copy(p, bytes, 0, length);
        return Encoding.UTF8.GetString(bytes);
    }
}

Mở một client cho máy này. Địa chỉ API, App ID và khoá công khai là ba giá trị ở trang Kết nối phần mềm; trong Unity, thư mục trạng thái nên là Application.persistentDataPath. Thư viện tự giữ khoá thiết bị và token offline mới nhất trong thư mục đó:

IntPtr client = Clavis.clavis_open(
    Clavis.Utf8(Env("CLAVIS_BASE_URL")),
    Clavis.Utf8(Env("CLAVIS_APP_ID")),
    Clavis.Utf8(Env("CLAVIS_APP_KEY")),
    // The device key and the last offline token live here, so sessions and offline use carry over between runs.
    Clavis.Utf8(Env("CLAVIS_STATE_DIR", ".clavis")),
    Clavis.Utf8("1.0.0"));
if (client == IntPtr.Zero)
{
    Console.WriteLine("FAIL settings");
    return 1;
}

Kích hoạt key của khách. Khi không có mạng, token offline lưu lần trước giữ game chạy tiếp:

int status = Clavis.clavis_init(client);
if (status == 0) status = Clavis.clavis_activate(client, Clavis.Utf8(Env("CLAVIS_LICENSE")));
if (status != 0)
{
    // No answer (network_error, timeout...): the offline token saved last time keeps the software working.
    if (Clavis.clavis_retryable(status) != 0 && Clavis.clavis_offline(client) == 0)
    {
        Console.WriteLine("Offline: using the license saved on this machine");
        Console.WriteLine("OK k1 offline");
        return 0;
    }
    Console.WriteLine(Clavis.Text(Clavis.clavis_error_message(client, lang)));
    Console.WriteLine("FAIL " + Clavis.Text(Clavis.clavis_error_code(client)));
    return 1;
}

Khoá công khai chỉ dùng để kiểm chữ ký, nhúng vào game là an toàn. Đừng bao giờ nhúng một khoá bí mật nào.

3. Đặt vào game

  • Mỗi lượt gọi chờ mạng tới vài giây, nên đừng gọi trên luồng chính của Unity: game sẽ đứng. Tạo một luồng nền (System.Threading.Thread) giữ client từ lúc mở tới lúc đóng.
  • Một client chỉ dùng trên một luồng. Luồng nền mở client, kích hoạt, rồi gửi heartbeat theo lịch; luồng chính chỉ đọc kết quả (trạng thái license, câu báo lỗi) mà luồng nền để lại, ví dụ trong Update().
  • Chuỗi thư viện trả về chỉ còn đúng tới lượt gọi kế tiếp trên client đó; Clavis.Text đã chép chúng sang chuỗi C#.
  • Gọi clavis_close khi game thoát (OnApplicationQuit).

4. Chạy thử

Trước khi đưa vào Unity, bạn có thể chạy cùng mã bằng .NET, không cần mở Unity:

  1. Ở trang Kết nối phần mềm, bấm Tạo key thử nghiệm.
  2. Tạo dự án console bằng dotnet new console -n ClavisCheck, xoá Program.cs và đặt file K1.cs đầy đủ dưới đây vào thay.
  3. Đặt thư mục giải nén của thư viện lên PATH, đặt bốn cài đặt rồi chạy dotnet run.
// K1 from C# through the C library: the way Unity, and any .NET that cannot take the C# SDK, calls
// ClavisAUH. Only DllImport and byte arrays, so Mono, IL2CPP and .NET Framework take it as it is (checked against
// netstandard2.0 and C# 9). Put clavis.dll (one file) beside the program or on the PATH, or libclavis.so /
// libclavis.dylib; in Unity, in Assets/Plugins.
// Settings: CLAVIS_BASE_URL, CLAVIS_APP_ID, CLAVIS_APP_KEY, CLAVIS_LICENSE, CLAVIS_STATE_DIR, CLAVIS_LANG.
using System;
using System.Runtime.InteropServices;
using System.Text;

static class Clavis
{
    const string Lib = "clavis";
    const CallingConvention Cdecl = CallingConvention.Cdecl;

    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_open(
        byte[] baseUrl, byte[] appId, byte[] appKey, byte[] stateDir, byte[] appVersion);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern void clavis_close(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_init(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_activate(IntPtr client, byte[] key);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_heartbeat(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_logout(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_offline(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_variable(IntPtr client, byte[] name);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_error_code(IntPtr client);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern IntPtr clavis_error_message(IntPtr client, byte[] lang);
    [DllImport(Lib, CallingConvention = Cdecl)]
    public static extern int clavis_retryable(int error);

    /// <summary>UTF-8 with its closing zero, as every clavis_* string argument.</summary>
    public static byte[] Utf8(string s) => Encoding.UTF8.GetBytes(s + "\0");

    /// <summary>A string the library returns: UTF-8, owned by the client until its next call. Read by hand, as
    /// Marshal.PtrToStringUTF8 is missing from .NET Framework and older Unity.</summary>
    public static string Text(IntPtr p)
    {
        if (p == IntPtr.Zero) return "";
        int length = 0;
        while (Marshal.ReadByte(p, length) != 0) length++;
        var bytes = new byte[length];
        Marshal.Copy(p, bytes, 0, length);
        return Encoding.UTF8.GetString(bytes);
    }
}

static class Program
{
    static string Env(string name, string fallback = "") => Environment.GetEnvironmentVariable(name) ?? fallback;

    static int Main()
    {
        // Messages in Vietnamese need a UTF-8 console.
        Console.OutputEncoding = Encoding.UTF8;
        byte[] lang = Clavis.Utf8(Env("CLAVIS_LANG", "en"));

        IntPtr client = Clavis.clavis_open(
            Clavis.Utf8(Env("CLAVIS_BASE_URL")),
            Clavis.Utf8(Env("CLAVIS_APP_ID")),
            Clavis.Utf8(Env("CLAVIS_APP_KEY")),
            // The device key and the last offline token live here, so sessions and offline use carry over between runs.
            Clavis.Utf8(Env("CLAVIS_STATE_DIR", ".clavis")),
            Clavis.Utf8("1.0.0"));
        if (client == IntPtr.Zero)
        {
            Console.WriteLine("FAIL settings");
            return 1;
        }

        try
        {
            int status = Clavis.clavis_init(client);
            if (status == 0) status = Clavis.clavis_activate(client, Clavis.Utf8(Env("CLAVIS_LICENSE")));
            if (status != 0)
            {
                // No answer (network_error, timeout...): the offline token saved last time keeps the software working.
                if (Clavis.clavis_retryable(status) != 0 && Clavis.clavis_offline(client) == 0)
                {
                    Console.WriteLine("Offline: using the license saved on this machine");
                    Console.WriteLine("OK k1 offline");
                    return 0;
                }
                Console.WriteLine(Clavis.Text(Clavis.clavis_error_message(client, lang)));
                Console.WriteLine("FAIL " + Clavis.Text(Clavis.clavis_error_code(client)));
                return 1;
            }

            // Every few minutes while the software runs; sessions end after an hour without one.
            Clavis.clavis_heartbeat(client);
            string motd = Clavis.Text(Clavis.clavis_variable(client, Clavis.Utf8("motd")));
            Console.WriteLine("Message of the day: " + motd);
            Clavis.clavis_logout(client);
            Console.WriteLine("OK k1 active");
            return 0;
        }
        finally
        {
            Clavis.clavis_close(client);
        }
    }
}
$env:PATH = "C:\duong-dan\clavis-c-1.0.0-windows-x64;$env:PATH"
$env:CLAVIS_BASE_URL = "<địa chỉ API>"
$env:CLAVIS_APP_ID = "<App ID>"
$env:CLAVIS_APP_KEY = "<khoá công khai>"
$env:CLAVIS_LICENSE = "<key thử nghiệm>"
dotnet run

Dòng cuối in ra OK k1 active. Trên Linux, thay PATH bằng LD_LIBRARY_PATH trỏ tới thư mục có libclavis.so.

5. Thấy trên dashboard

  • Tích hợp › Kết nối phần mềm: bước 4 chuyển sang Đã kết nối, kèm c/1.0.0 (thư viện C) và hệ điều hành của máy.
  • Phát hành & bản quyền › License: key chuyển từ Chưa dùng sang Đang dùng.
  • Người dùng cuối › Thiết bị & phiên: máy vừa chạy, cùng phiên nó vừa mở.

6. Xử lý lỗi

Mọi hàm trả int trả 0 khi thành công, hoặc một số lỗi cố định (bảng Mã lỗi, cột C). Sau một lần thất bại:

  • clavis_error_code(client): mã dạng chữ, ví dụ invalid_credentials hay device_limit.
  • clavis_error_message(client, "vi"): câu cho người chơi, tiếng Việt hoặc "en".
  • clavis_retryable(số lỗi): khác 0 khi không nhận được câu trả lời; thử lại sau có thể thành công. Lúc đó clavis_offline(client) trả 0 nếu token offline đã lưu còn dùng được trên máy này.

Khi game đang chạy, gửi heartbeat đều đặn từ luồng nền, khoảng mỗi 5 tới 30 phút: phiên hết sau 60 phút không có heartbeat.

// Every few minutes while the software runs; sessions end after an hour without one.
Clavis.clavis_heartbeat(client);
string motd = Clavis.Text(Clavis.clavis_variable(client, Clavis.Utf8("motd")));
Console.WriteLine("Message of the day: " + motd);

Mọi gói đều có token offline, mặc định dùng được 7 ngày; Premium tự chọn thời hạn, còn Chế độ chỉ online thì tắt hẳn. Xem License offline.

7. Build và phát hành

  • Ghi địa chỉ API, App ID và khoá công khai vào script hoặc ScriptableObject của game.
  • Unity chép plugin vào thư mục Plugins của bản build. Kiểm bản build trên một máy sạch: game phải tới được trạng thái đã kích hoạt mà không cần cài gì thêm (Windows) hoặc chỉ cần libsodium và libcurl (Linux).
  • Truyền phiên bản game vào clavis_open (tham số cuối): server dùng nó để chặn bản quá cũ và báo có bản mới, xem Bản phát hành có ký và cập nhật.
  • Trước khi bán, bật Đã phát hành công khai ở Tích hợp › Cài đặt ứng dụng. Mọi key TEST ngừng hoạt động, nên key thử nào lỡ lọt ra ngoài cũng không dùng được nữa.

Câu hỏi thường gặp

Sao không dùng SDK C#? SDK C# cần .NET 8, Unity chưa chạy được. Thư viện C là cùng SDK C++ bên dưới, cùng giao thức và cùng cách kiểm chữ ký.

Bản Android, iOS, macOS? Chưa có. Trang Tải về liệt kê các bản đang có.

Giới hạn tính năng theo gói license thế nào? clavis_has(client, "ten_tinh_nang") cho biết license có quyền tính năng đó không; thêm khai báo DllImport cho nó giống các hàm trong lớp Clavis. Danh sách đủ các hàm có trong include/clavis.h của gói tải về.

Khách cài lại hệ điều hành hay đổi máy thì sao? Cài lại trên cùng máy, server vẫn nhận ra máy nhờ dấu nhận diện phần cứng đã băm. Máy mới chiếm một suất mới; hết suất thì khách nhận device_limit, và bạn Đặt lại thiết bị trong trang License. Xem Thiết bị, HWID và chuyển máy.

Liên quan

Bạn cần giúp gì?

Câu hỏi thường gặp

ClavisAUH là gì?

Dịch vụ bản quyền và xác thực cho phần mềm desktop: license key, khoá thiết bị, đăng nhập người dùng và phản hồi có chữ ký. Tất cả quản lý trên một dashboard

Vì sao kẻ crack không làm giả được server?

SDK chỉ tin câu trả lời ký bằng khoá bí mật của ứng dụng. Khoá đó không bao giờ rời server của chúng tôi, nên server giả không ký được và bị từ chối

Trong file chạy của tôi có gì?

Mã ứng dụng và khoá công khai. Không có bí mật nào, vì mỗi thiết bị tự tạo cặp khoá riêng ở lần chạy đầu

Có chạy được khi offline không?

Có. Mọi gói đều có token offline có chữ ký dùng được 7 ngày, ứng dụng tự kiểm tra tại máy. Premium cho bạn chọn bao lâu. License đã thu hồi vẫn có thể chạy offline trong khoảng đó, trừ khi Chế độ chỉ online đang bật

Hỗ trợ những ngôn ngữ nào?

Hiện có JavaScript và TypeScript. Tiếp theo là C++, C# và Python, cùng một giao thức công khai

Gói Basic có miễn phí thật không?

Có. Không giới hạn thời gian, không cần thẻ. Cần nhiều hơn thì lên Premium

Liên hệ