카테고리: 아크 서버 플러그인

아크 서버 플러그인 개발 기초 - ArkServerAPI 구조 완전 해설 (Load·Unload·명령어 등록) 🦖

목차11개 항목

안녕하세요! Mojangcam입니다 😎

예전에 아크 서바이벌 플러그인 만들기 - 명령어 글을 올렸는데, 지금까지도 가장 꾸준히 읽히는 글 중 하나예요. 그만큼 아크 서버 운영하시는 분들이 "내 서버에만 있는 기능"을 많이 원하시는 거겠죠.

근데 막상 따라 하다 보면 "그래서 이 코드는 왜 이렇게 생겼지?" 가 헷갈리세요. Load()는 뭐고 DllMain은 왜 있는지, 명령어는 왜 세 종류나 되는지 말이에요.

그래서 오늘은 복붙 전에 플러그인 하나의 골격을 통째로 보여드릴게요. 이 구조만 머릿속에 들어가면 그다음부터는 기능만 갈아끼우면 돼요 🙌

※ 이 글은 ARK: Survival Evolved(서바이벌 이볼브드) + 윈도우 서버 기준입니다.

▲ 폴더명과 .dll 이름이 같아야 하고, 서버 시작 로그에 플러그인이 잡히면 성공입니다

🧩 ArkServerAPI가 뭔가요?

아크 서버는 마인크래프트의 Bukkit처럼 공식 플러그인 API가 없어요. 그래서 커뮤니티가 만든 게 바로 ArkServerAPI입니다.

  • 서버 실행 파일에 끼어들어 게임 내부 함수를 후킹(Hook) 해주는 프레임워크예요.
  • 플러그인은 C++로 작성해서 .dll로 빌드합니다. (파이썬·자바 아니에요!)
  • 현재 GameServersHub에서 유지보수하고 있고, MIT 라이선스 오픈소스입니다.
  • Studio Wildcard 공식 프로젝트가 아니에요. 팬 프로젝트라는 점은 알고 쓰셔야 해요.

📌 준비물

  • 운영체제: Windows 7 / Windows Server 2008 이상 (리눅스 서버는 미지원)
  • Microsoft Visual C++ 2019 재배포 패키지 (x64) — 이거 안 깔면 서버가 튀어요
  • Visual Studio 2019 이상 + "C++를 사용한 데스크톱 개발" 워크로드
  • ArkServerAPI 본체 — GitHub 저장소 또는 GameServersHub에서 다운로드
  • 테스트용 아크 전용 서버 (운영 서버에 바로 올리지 마세요!)

설치는 압축 푸는 게 끝이에요.

tex
# API 설치: 압축 파일 내용을 이 경로에 풀기
\ARK\ShooterGame\Binaries\Win64\

# 플러그인 설치: 이 경로에 플러그인 폴더를 통째로 넣기
\ARK\ShooterGame\Binaries\Win64\ArkApi\Plugins\

⚠️ 가장 많이 틀리는 부분: .dll 파일 이름과 상위 폴더 이름이 반드시 같아야 해요. Plugins\HelloPlugin\HelloPlugin.dll 이런 식으로요.

📌 1단계 - 플러그인 폴더 구조 파악하기

완성된 플러그인 하나는 보통 이렇게 생겼습니다.

코드
ArkApi\Plugins\HelloPlugin\
  ├─ HelloPlugin.dll      ← 빌드 결과물 (폴더명과 동일!)
  ├─ PluginInfo.json      ← 플러그인 메타정보 (버전·의존성)
  └─ config.json          ← (선택) 설정값

로그는 여기서 확인합니다. 문제 생기면 제일 먼저 여는 파일이에요.

  • 일반 로그 : \ARK\ShooterGame\Win64\logs\ArkApi.log
  • 크래시 로그 : \ARK\ShooterGame\Saved\Logs

📌 2단계 - 플러그인의 뼈대: DllMain → Load → Unload

여기가 오늘의 핵심이에요. 플러그인은 결국 DLL 하나라, 서버가 이 DLL을 메모리에 올릴 때와 내릴 때 호출되는 지점이 있습니다.

Groovy
BOOL APIENTRY DllMain(HMODULE, DWORD ul_reason_for_call, LPVOID)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:   // 플러그인이 메모리에 올라갈 때
        Load();
        break;
    case DLL_PROCESS_DETACH:   // 내려갈 때 (서버 종료 등)
        Unload();
        break;
    }
    return TRUE;
}

하나씩 뜯어볼게요 💡

  • DllMain : 윈도우가 DLL을 로드/언로드할 때 불러주는 표준 진입점이에요. ArkServerAPI가 따로 만든 게 아니라 윈도우 규격입니다.
  • DLL_PROCESS_ATTACH : 붙을 때 한 번. 여기서 Load()를 불러 초기화합니다.
  • DLL_PROCESS_DETACH : 떼어질 때 한 번. 여기서 Unload()로 등록한 걸 전부 되돌려야 해요.
  • 이 짝을 안 맞추면 리로드할 때 명령어가 중복 등록되거나 후킹이 남아서 서버가 튀어요.

그리고 Load() 안에서 제일 먼저 해야 하는 건 로거 초기화입니다.

코드
Log::Get().Init("HelloPlugin");        // 플러그인 전용 로거 생성
Log::GetLog()->info("HelloPlugin 로드 완료");

📌 3단계 - 명령어 3종 등록하기 (채팅·콘솔·RCON)

ArkServerAPI는 명령어를 세 가지 통로로 나눠서 받아요. 세 개의 함수 시그니처가 서로 다르니 이건 꼭 외워두세요.

종류 등록 함수 누가 쓰나
채팅 명령어 AddChatCommand("/hello", &Fn) 게임 안 플레이어
콘솔 명령어 AddConsoleCommand("Hello", &Fn) 서버 콘솔·관리자
RCON 명령어 AddRconCommand("Hello", &Fn) 외부 RCON 툴·디스코드 봇

각각의 콜백 시그니처는 이렇게 생겼어요.

코드
// 채팅
void Fn(AShooterPlayerController* pc, FString* message, EChatSendMode::Type mode);

// 콘솔
void Fn(APlayerController* pc, FString* message, bool write_to_log);

// RCON
void Fn(RCONClientConnection* conn, RCONPacket* packet, UWorld* world);

하나씩 뜯어볼게요 💡

  • AShooterPlayerController* : 명령어를 친 플레이어예요. 여기서 스팀 ID·캐릭터·레벨을 다 끌어올 수 있습니다.
  • FString* : 플레이어가 입력한 메시지 전체예요. /give 5 같은 인자를 파싱할 때 쓰죠.
  • EChatSendMode::Type : 전체채팅인지 부족채팅인지 구분입니다.
  • RCONClientConnection* : 응답을 되돌려줄 연결 객체. conn->SendMessageW(packet->Id, 0, &reply) 로 답장을 보냅니다.

메시지 보내는 방법도 두 가지로 나뉩니다.

  • SendChatMessage(pc, 보내는사람, 내용) : 채팅창에 대사처럼 표시
  • SendServerMessage(pc, 색상, 내용) : 화면 중앙에 서버 메시지로 표시 (FColorList::Green 등)

📌 4단계 - PluginInfo.json 작성하기

플러그인 폴더에 같이 넣는 메타정보 파일이에요. 이게 없거나 형식이 틀리면 로드가 안 됩니다.

JSON
{
  "FullName": "Hello Plugin",
  "Description": "첫 플러그인 예제",
  "Version": 1.0,
  "MinApiVersion": 3.5,
  "Dependencies": []
}
  • Version : 내 플러그인 버전
  • MinApiVersion : 이 플러그인이 요구하는 최소 API 버전. 서버의 API가 이보다 낮으면 로드를 거부해요. (위 값은 예시니, 본인 서버에 깔린 API 버전에 맞춰주세요)
  • Dependencies : 먼저 로드돼야 하는 다른 플러그인 (예: ["Permissions"])

권한 관리가 필요한 플러그인이라면 Permissions 플러그인을 의존성으로 걸고 Permissions::IsPlayerHasPermission(steam_id, "내플러그인.권한이름") 형태로 체크하면 돼요.

📌 전체 코드 (복붙 OK)

C++
// ArkServerAPI · ARK: Survival Evolved (Windows) 전용
// Visual Studio 2019+ / 구성: Release / 플랫폼: x64 / 구성 종류: 동적 라이브러리(.dll)

#include <API/ARK/Ark.h>
#include <API/UE/Math/ColorList.h>

#pragma comment(lib, "ArkApi.lib")

// ---------- 채팅 명령어: /hello ----------
void HelloChatCommand(AShooterPlayerController* player_controller, FString* /*message*/,
                      EChatSendMode::Type /*mode*/)
{
    if (ArkApi::IApiUtils::IsPlayerDead(player_controller))
        return;

    const uint64 steam_id = ArkApi::IApiUtils::GetSteamIdFromController(player_controller);
    Log::GetLog()->info("/hello 사용자 SteamID: {}", steam_id);

    ArkApi::GetApiUtils().SendChatMessage(player_controller, L"서버",
                                          L"안녕하세요! 플러그인이 정상 동작 중입니다.");
}

// ---------- 콘솔 명령어: Hello ----------
void HelloConsoleCommand(APlayerController* player_controller, FString* /*message*/, bool /*write_to_log*/)
{
    auto* shooter_player = static_cast<AShooterPlayerController*>(player_controller);

    ArkApi::GetApiUtils().SendServerMessage(shooter_player, FColorList::Green,
                                            L"HelloPlugin 작동 중!");
}

// ---------- RCON 명령어: Hello ----------
void HelloRconCommand(RCONClientConnection* conn, RCONPacket* packet, UWorld* /*world*/)
{
    FString reply = L"HelloPlugin 작동 중!";
    conn->SendMessageW(packet->Id, 0, &reply);
}

// ---------- 플러그인 진입점 ----------
void Load()
{
    Log::Get().Init("HelloPlugin");

    ArkApi::GetCommands().AddChatCommand("/hello", &HelloChatCommand);
    ArkApi::GetCommands().AddConsoleCommand("Hello", &HelloConsoleCommand);
    ArkApi::GetCommands().AddRconCommand("Hello", &HelloRconCommand);

    Log::GetLog()->info("HelloPlugin 로드 완료");
}

// ---------- 플러그인 종료점 (등록한 건 반드시 되돌리기) ----------
void Unload()
{
    ArkApi::GetCommands().RemoveChatCommand("/hello");
    ArkApi::GetCommands().RemoveConsoleCommand("Hello");
    ArkApi::GetCommands().RemoveRconCommand("Hello");
}

BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        Load();
        break;
    case DLL_PROCESS_DETACH:
        Unload();
        break;
    }
    return TRUE;
}

빌드 설정은 이 네 가지만 맞추면 됩니다.

  1. 구성 종류 : 동적 라이브러리(.dll)
  2. 플랫폼 : x64 (x86으로 빌드하면 로드 자체가 안 돼요)
  3. 추가 포함 디렉터리 : API의 include 폴더
  4. 추가 라이브러리 디렉터리 : API의 lib(또는 out_lib) 폴더

💡 언어 표준은 C++17 이상으로 잡아두는 게 안전해요. API 헤더가 최신 문법을 써서 구버전 표준에선 컴파일 에러가 쌓입니다.

▲ 구성 종류(.dll)·플랫폼(x64)·포함 디렉터리·라이브러리 디렉터리 네 곳만 맞추면 됩니다

📌 실행 결과

빌드한 HelloPlugin.dll과 PluginInfo.json을 ArkApi\Plugins\HelloPlugin\ 에 넣고 서버를 재시작해보세요. (플러그인은 핫 리로드가 안 돼요. 서버를 다시 올려야 합니다.)

  • 로그에 HelloPlugin 로드 완료 가 찍혔다면 성공!
  • 게임 채팅창에 /hello 를 치면 서버 응답이 돌아옵니다. (스크린샷은 실제 서버에서 찍어 추가할 예정이에요)
  • RCON 툴에서 Hello 를 보내면 동일한 메시지가 답장으로 옵니다.

▲ 문제가 생기면 이 로그 파일부터 여세요 — 로드 여부가 여기 다 찍힙니다

📌 자주 나는 에러

1. 플러그인이 아예 로드가 안 돼요 (로그에 아무 흔적도 없음)

  • 원인 ①: .dll 파일명 ≠ 상위 폴더명. 제일 흔한 실수예요.
  • 원인 ②: 경로가 틀림. Binaries\Win64\ArkApi\Plugins\ 아래가 맞는지 확인하세요.
  • 원인 ③: PluginInfo.json 이 없거나 JSON 문법이 깨짐(마지막 콤마 주의!).

2. 서버가 켜지자마자 튀어요

  • 원인 ①: Visual C++ 2019 재배포 패키지(x64) 미설치.
  • 원인 ②: 플러그인을 x86으로 빌드함. 반드시 x64여야 해요.
  • 원인 ③: 게임 업데이트 직후. 아크가 패치되면 API가 참조하는 메모리 오프셋이 틀어집니다. → API 업데이트가 나올 때까지 서버 자동 업데이트를 잠시 막아두세요. 아크 플러그인 서버 운영의 숨은 복병이에요.

3. LNK2019 - 외부 기호를 찾을 수 없음

  • 원인: ArkApi.lib 를 링커가 못 찾는 경우예요.
  • 해결: #pragma comment(lib, "ArkApi.lib") 가 있는지, 추가 라이브러리 디렉터리에 API의 lib 폴더를 넣었는지 확인하세요.

4. 한글 메시지가 깨져서 나와요

  • 원인: JSON 설정 파일에서 읽은 UTF-8 문자열을 그대로 넘겼을 때 생깁니다.
  • 해결: ArkApi::Tools::Utf8Decode(문자열) 로 변환해서 FString 으로 만들어 보내세요.

5. 리로드하니 명령어가 중복으로 등록돼요

  • 원인: Unload() 에서 Remove... 를 빼먹은 경우예요.
  • 해결: Load()에서 등록한 명령어·후킹은 1:1로 전부 Unload()에서 해제하세요.

💬 마무리

오늘은 아크 플러그인의 골격을 봤어요. 정리하면 딱 세 줄입니다.

  1. 플러그인은 x64 DLL이고, DllMain에서 Load() / Unload() 로 갈라진다
  2. Load()에서 로거 초기화 → 명령어·후킹 등록, Unload()에서 전부 해제
  3. 폴더명과 .dll 이름을 맞추고 PluginInfo.json을 같이 넣는다

다음 아크 편에서는 커스텀 명령어 확장으로 인자 파싱·권한 체크·설정 파일 연동까지 붙여볼게요. 기대해주세요 🙌

📚 함께 보면 좋은 글

글쓴이

Mojangcam

직접 코드를 짜고, 서버를 돌리고, 막힌 에러를 해결하며 배운 것들을 기록하는 개발·IT 실전 기록 블로그