Skip to content
스토브
마지막 업데이트

스팀 연동

이해하기

스팀 연동은 스팀런처로 배포·실행되는 게임이 스토브 플랫폼 기능을 사용할 수 있도록 이어 주는 기능이에요. 스팀런처로 게임을 실행했을 때 스팀 로그인 정보를 가지고 스토브 플랫폼 인증을 거쳐, 스토브런처로 실행한 것과 똑같이 PCSDK3(BaseSDK) 를 기동해 주는 징검다리 역할을 해요. 연동에는 스토브 PC SDK가 제공하는 별도의 외부플랫폼연동모듈(APIModule) 을 사용해요.

이건 꼭 알아 두세요

외부플랫폼연동모듈은 선택 적용 항목입니다. 게임을 스팀런처에서도 배포·실행하면서 스토브 플랫폼 기능을 사용하려는 경우에만 연동하면 되고, 이때는 이 모듈과 PCSDK3를 함께 연동해요. 스토브런처로만 게임을 배포한다면 이 모듈은 필요 없고 PCSDK3만 연동하면 돼요.

적용 환경 및 동작 범위

항목 내용
제공 환경 PC SDK 3.0 (외부플랫폼연동모듈) 전용. PCSDK3와 짝을 이뤄 동작.
지원 플랫폼 2026년 7월 현재 스팀(Steam)만 지원. 초기화 시 PlatformName"STEAM" 고정.
적용 여부 선택 적용. 스팀런처 배포 + 스토브 플랫폼 기능 사용 게임에 한해 연동.
모듈 담당 범위 스토브 플랫폼 서버와 통신해 액세스 토큰 등 필수 데이터를 확보하고 PCSDK3로 전달하는 것까지.
개발사 구현 범위 사용자에게 보여줘야 하는 모든 안내 화면(UI). 약관 화면, 접속 불가·제재·점검 안내, 오류 팝업 등.
Steamworks SDK 모듈에 포함되지 않음. 게임이 Steamworks SDK를 별도로 연동·초기화한 상태를 전제로 동작.
미지원 기능 스팀 빌드 + 외부플랫폼연동모듈 PCSDK3 동선에서는 Ownership, GameSupport(스탯·업적·리더보드 등) 미지원.

안내 UI는 전부 개발사가 구현해요.

연동하는 게임 엔진과 환경이 개발사마다 달라서 공통 UI를 제공하기 어렵기 때문이에요. SDK는 상황을 결과 코드로만 알려주고, 화면은 개발사가 인게임 UI(또는 인게임 웹브라우저)로 그려요.

진입 흐름 한눈에 보기

게임 진입 체크 결과에 따라 크게 세 갈래로 나뉘어요.

  • 성공(0) → 토큰·회원 정보를 받아 곧바로 PCSDK3 기동으로 이어가요.
  • 약관 동의 필요(406401) → 약관을 조회·표시하고 사용자 동의를 받은 뒤, 게임 진입 체크를 다시 호출해요.
  • 그 외 오류 → 결과 코드에 맞는 안내를 보여주고 게임을 종료해요.

서버는 대략 차단 IP → 스팀 게임 약관 동의 → 게임 점검 → 게임 제재 → 그 외 오류 순서로 진입 가능 여부를 평가한 뒤 하나의 결과 코드를 내려줘요. 클라이언트는 결과 코드 하나만 보고 분기하면 돼요.

계정 유형 — Shadow와 정회원

스팀 인증 정보로 게임 진입 체크를 하면, 계정은 두 가지 중 하나예요.

유형 설명
Shadow 계정 스팀에 가입되어 있으나 스토브 정회원 전환은 하지 않은 계정. 스토브 플랫폼 약관 동의 없이 스팀 게임 약관만 동의한 스팀 전용 임시 계정이에요. 스토브 회원번호(member_no)가 없고, 약관 동의 시 GUID가 발급돼요.
정회원 스팀 가입 + 스토브 정회원 전환을 완료한 계정. 스토브 회원번호(member_no)를 가져요.

SDK·게임은 두 유형을 구분하지 않아요.

게임 진입 체크 동선에서 개발사가 계정 유형(Shadow/정회원)을 구분해 분기할 필요가 없어요. 두 유형 모두 동일하게 동작해요. 정회원 전환은 스토브 웹 페이지에서 진행되며, 이 모듈은 정회원 전환 전용 API를 제공하지 않아요. 모듈이 담당하는 범위는 스팀 게임 약관 조회·동의까지예요.

비동기 콜백과 콜백 펌프

이 모듈에서 시간이 걸리는 기능(초기화·게임 진입 체크·약관 조회·약관 동의)은 모두 비동기로 동작해요. 함수를 호출한다고 결과가 바로 오는 게 아니라 콜백으로 전달돼요.

콜백을 실제로 받으려면 펌프 함수 Stove_APIModule_RunCallback()을 게임 루프에서 반복 호출해야 해요. 이 콜백은 펌프를 호출한 스레드에서 실행되므로, 반드시 게임 메인 스레드에서 펌프를 호출해요. 그러면 대부분의 게임 UI·로직과 같은 스레드에서 콜백이 실행돼 동기화 부담을 줄일 수 있어요.

연동 가이드

연동 준비

항목 내용
Steamworks SDK 스팀 기본 연동은 Steamworks 공식 문서 참고. 세션 토큰·앱 아이디·유저 아이디 발급에 필요.
스팀 세션 토큰 ISteamUser::GetAuthTicketForWebApi로 발급. 매번 새로 얻는 값이라 초기화·게임 진입 체크 직전에 발급받아 사용.
게임 아이디(GameId) 스토브 플랫폼에 등록된 게임별 고유 ID. SGP 퍼블리싱기술실에 문의.
스팀 앱 아이디(SteamAppId) 스팀 앱 등록 시 발급되는 숫자 일련번호.
스팀 유저 아이디(SteamUserId) 17자리 숫자. ISteamUser::GetSteamID().ConvertToUint64()를 문자열로 변환해 사용.
공개 헤더 · DLL 배포되는 DLL과 공개 헤더 4종(별도의 레퍼런스 문서공개 헤더 구성 참고)을 프로젝트에 포함.
콜백 구동 환경 SDK 비동기 콜백 처리를 위해 게임 메인 스레드에서 Stove_APIModule_RunCallback() 주기 호출.
PCSDK3 연동 인증 성공 후 이어서 PCSDK3(BaseSDK)를 기동해야 하므로 PCSDK3 연동이 선행/병행되어야 함.

개발하기

PC (PCSDK) — 외부플랫폼연동모듈(APIModule)

스팀런처 배포 게임 전용 기능입니다.

외부플랫폼연동모듈 연동은 선택 적용 항목이에요. 인증을 마친 뒤에는 반드시 PCSDK3 기동으로 이어져야 하며, 이 모듈만 단독으로 쓰이지는 않아요.

사전 준비

  • 공개 헤더(api_module.h, api_module_types.h)를 include해요. C에서는 멤버 접근용 api_module_flat.h도 함께 include해요.
  • 게임 루프에서 Stove_APIModule_RunCallback()게임 메인 스레드에서 주기적으로 호출돼야 콜백이 정상 전달돼요.
  • 콜백 함수는 헤더의 콜백 typedef(void(__cdecl* ...))과 호출 규약을 맞추기 위해 __cdecl로 선언해요. C#은 CallingConvention.Cdecl로 맞춰져 있어요.
  • 콜백으로 받은 결과 객체는 모듈 소유라 콜백 실행 중에만 유효해요. 밖에서 쓰려면 콜백 안에서 값을 복사(문자열은 깊은 복사)해요. 파라미터 객체와 동기 함수 반환 객체는 호출자가 정리(Destroy) 해요. 자세한 규칙은 별도의 레퍼런스 문서객체 수명 관리를 참고하세요.

개발 흐름

  1. 초기화 : Stove_APIModule_Initialize(param, onFinished, userData)로 모듈을 초기화해요. 파라미터에는 실행 환경·플랫폼 이름("STEAM")·스팀 앱 아이디·스팀 유저 아이디를 담아요. 비동기이므로 성공 콜백을 받은 뒤에야 게임 진입 체크를 호출할 수 있어요.
  2. 게임 진입 체크 : Stove_APIModule_GameCheckerForSteam(param, onFinished, userData)를 호출해요. 스팀 세션 토큰과 게임 아이디를 넘기면, 이 게임에 진입해도 되는지 서버가 판단해서 PCSDK3 구동에 필요한 정보를 결과로 돌려줘요. : 콜백에서 성공 여부는 GetResult()->IsSuccessful()로, 실패 시 분기 코드는 GetExternalError()로 확인해요.
  3. 결과 코드 분기 : 결과 코드에 따라 화면 처리가 달라져요. (전체 표는 별도의 레퍼런스 문서결과 코드를 참고하세요.)
    • 0 (Success) → 4단계 PCSDK3 기동으로 진행
    • 406401 (NotAgreeTerms) → 5단계 약관 동선으로 진행
    • 그 외 → 결과별 안내 팝업 후 게임 종료
  4. PCSDK3 기동 : 게임 진입 체크가 성공하면 이어서 PCSDK3를 비동기로 기동해요. Base_RestartAppIfNecessaryAsyncBase_InitializeEx 순서로 호출하며, 게임 진입 체크로 확보한 접속 정보는 모듈이 내부 IPC로 PCSDK3에 전달하므로 개발사가 토큰을 직접 넘길 필요는 없어요. 자세한 내용은 PCSDK3 문서를 참고하세요.
  5. 약관 조회와 동의 (406401일 때만) : Stove_APIModule_FetchGameTermsForSteam(param, ...)으로 약관 배열을 조회하고, 한 화면에 조합해 보여줘요. 사용자가 동의하면 Stove_APIModule_AgreeToGameTermsForSteam(param, ...)으로 제출하고, 응답 guid를 받아요. : 조회 시 AgType은 스팀 게임서비스 약관을 뜻하는 1 (Steam)을 넘겨요. 응답 배열에는 스팀 게임서비스 약관(AgreeType = "FIRST_MUST", 필수 동의)이 내려오며, 동의 유형은 배열 인덱스가 아니라 GetAgreeType() 값으로 판단하세요. : 제출이 성공하면 서버에 동의 상태가 반영되므로, 게임 진입 체크를 다시 호출하면 406401이 더는 발생하지 않아요. : 신규 스팀 유저(스토브 회원 없음)의 동의 제출은 스토브 백엔드에서 Shadow 계정 생성을 완료하고 guid를 발급해요. 모듈 자체에는 Shadow 생성 로직이 없어요. 개발사가 이 guid로 별도 매핑을 할 필요는 없고, 게임 진입 체크를 재호출하면 정상 진입돼요. : 화면을 어떻게 구성하고 어떤 값을 채우는지는 아래 약관 동의 화면 만들기 를 참고하세요.
  6. 정리 : 게임 종료 시 Stove_APIModule_UnInitialize()로 모듈을 정리해요. 이 함수는 동기로 동작하며 결과 객체를 반환하는데, 확인한 뒤 호출자가 Destroy()로 정리해요.
  7. 버전 확인(선택) : 기술지원 문의 시 Stove_APIModule_GetVersion(buffer, length)으로 받은 버전 문자열을 함께 전달해요.

스팀런처로 구동되면 결제(IAP) 동선도 달라져요.

스팀런처로 실행하면 결제가 스토브 웹 결제가 아니라 스팀 결제(스팀 오버레이 구매창) 로 처리되고, 스토브 플랫폼에는 백엔드가 스팀 구매 내용을 연동해요. 특히 스팀런처에는 자동 구매확정 동선이 없어 구매 수락 후 구매 확정(ConfirmPurchase) 호출이 필수예요. 다만 이 부분은 외부플랫폼연동모듈이 아니라 PCSDK3(BaseSDK) 결제 API 영역이므로, 구체적인 API와 옵션은 PCSDK3 결제 문서를 참고하세요.

안내 화면 문구는 어디에서 오나요

안내 화면은 개발사가 그리지만, 화면에 들어갈 문구를 개발사가 전부 창작해야 하는 것은 아니에요. 세 가지 결과 코드는 표시할 내용을 SDK 가 콜백으로 함께 내려줘요. 받은 값을 그대로 화면에 뿌리면 돼요.

결과 코드SDK 가 내려주는 값어디에서 받나요
406401 약관 동의 필요약관 제목 · 약관 본문 · 시행 일시 · 동의 유형약관 조회(Stove_APIModule_FetchGameTermsForSteam) 콜백의 약관 항목
403201 게임 제재제재 유형 표시 문구 · 제재 사유 · 제재 시작·종료 일시게임 진입 체크 콜백 결과 객체의 제재 정보
503100 게임 점검점검 공지 제목 · 점검 공지 본문 · 점검 시작·종료 일시게임 진입 체크 콜백 결과 객체의 점검 정보

그 밖의 실패 코드는 부가 정보 없이 코드만 내려와요. 이때 보여 줄 문구는 개발사가 정해요. 레퍼런스 문서의 결과 코드 표에 코드별 권장 문구 예시가 있으니 그대로 쓰거나 게임 톤에 맞게 다듬어 쓰세요.

개발사가 직접 쓰는 문구와, SDK 가 내려주는 문구를 구분하세요.

화면 제목 · 버튼 라벨 · 체크박스 라벨 · 날짜 표기 형식은 개발사가 정해요. 약관 본문 · 제재 사유 · 점검 공지 본문은 SDK 가 내려주는 값을 그대로 보여줘야 해요. 임의로 요약하거나 바꾸면 안 돼요.

406401 만 값을 받는 방법이 달라요.

403201 · 503100 은 게임 진입 체크 콜백 안에서 곧바로 값을 꺼낼 수 있어요. 406401 은 게임 진입 체크 콜백에 약관 본문이 들어 있지 않아요. 약관 조회를 한 번 더 호출해서 받아야 해요.

약관 동의 화면 만들기

화면 구성 예시

약관 화면에 반드시 있어야 하는 요소는 다음 다섯 가지예요. 약관 본문을 어디에 보여 줄지는 A. 목록 + 별도 화면 보기B. 아코디언형 가운데 게임에 맞는 쪽을 고르면 돼요. 개인정보 항목은 한국 유저와 한국 외 글로벌 유저에게 다르게 표시하므로, 아래 그림에 네 가지 화면을 함께 담았어요.

아래 그림은 그대로 구현하기 위한 디자인 시안이 아니라, 어떤 요소가 필요한지 확인하는 구성 참조용이에요. 배치와 디자인은 게임 UI 에 맞춰 자유롭게 정하면 돼요.

약관 동의 화면 구성 예시
화면에 채우는 값
화면 요소값의 출처비고
약관 제목약관 항목의 Title그대로 표시해요
약관 본문약관 항목의 TextHTML 로 전달돼요. 길이가 길어서 스크롤이 필요해요
동의 유형약관 항목의 AgreeType서버가 내려주는 분류값이에요. FIRST_MUST 는 최초 동의가 필요한 약관이에요. 배열 순서로 판단하지 마세요
시행 일시약관 항목의 EnforcedDtUnix epoch 밀리초예요. 화면에 표시할지와 표기 형식은 게임에서 정해요
약관 항목 개수결과 객체의 항목 개수여러 건이 내려올 수 있어요. 전부 보여줘야 해요
화면 제목 · 버튼 · 체크박스 라벨개발사SDK 가 내려주지 않아요
개인정보 항목은 지역에 따라 달라요

내려온 약관 가운데 개인정보 항목은 한국 유저와 한국 외 글로벌 유저에게 다르게 표시해요.

지역화면에 표시할 항목동의 체크
한국개인정보 수집 및 이용 안내고지 항목이라 체크를 두지 않아요
한국 외 글로벌개인정보처리방침동의 항목이라 체크를 둬요
동의 처리 규칙
  • 동의 제출은 항목 단위가 아니라 게임 단위예요. 동의 제출 API 에 넘기는 값은 게임 아이디와 스팀 세션 토큰뿐이며, 어떤 항목에 동의했는지는 보내지 않아요. 화면에서 체크박스를 항목별로 두더라도, 제출은 한 번만 하면 돼요.
  • 내려온 약관을 한 화면에 모아 보여 주고, 동의 체크는 항목마다 하나씩 둬요. 고지 항목에는 체크를 두지 않아요.
  • 동의 버튼을 누르면 위 항목이 모두 체크되고 동의가 제출돼요. 사용자가 체크박스를 하나씩 누르지 않아도 되며, 체크 여부로 버튼을 막지 않아요.
  • 사용자가 동의하지 않으면 게임에 진입할 수 없어요. 안내 후 게임을 종료하세요.
  • 동의 제출이 성공하면 게임 진입 체크를 다시 호출해요. 이때는 406401 이 나오지 않아요.
  • 동의 제출 콜백에서 성공을 먼저 확인하고 재호출하세요. 실패했는데 재호출하면 406401 이 반복돼요.
약관 본문의 형식

약관 본문은 HTML 로 전달돼요. 스토브 파트너스에 입력된 값을 그대로 전달하므로 기본적으로 HTML 이 담겨 와요. 본문이 길고 줄바꿈이 들어 있으니 다음 두 가지를 지켜 주세요.

  • 줄바꿈 문자를 그대로 살려서 문단 구분이 유지되도록 해요.
  • 스크롤 가능한 영역에 넣어요. 잘라내거나 요약하면 안 돼요.

SDK 나 서버는 본문 형식을 가공하지 않아요. 게임 UI 에서 HTML 을 그대로 표시하기 어렵다면 평문으로 바꿔 받을 수 있어요.

약관 본문은 HTML 로 전달돼요.

게임 UI 구현상 HTML 을 그대로 표시하기 어렵다면 평문으로 바꿔 받을 수 있어요. 기술지원으로 문의해 주세요. 메일문의 : stove.developers@smilegate.com

약관 본문의 언어

약관 본문은 SDK 에 설정된 언어로 내려와요. 약관을 조회하기 전에 Stove_APIModule_SetLanguage() 로 게임의 표시 언어를 맞춰 두세요. 언어를 맞추지 않으면 게임 화면과 약관 본문의 언어가 어긋날 수 있어요.

전체 연동 시퀀스

트러블슈팅

상황원인해결 방법
초기화 콜백이 성공했는데도 게임 진입 체크가 계속 400000 (BadRequest)로 떨어져요스팀 세션 토큰이 만료·재사용됐어요. GetAuthTicketForWebApi로 받은 토큰은 매번 새로 얻는 값이라, 예전에 캐시해 둔 값을 다시 넘기면 서버가 거절해요.게임 진입 체크(또는 초기화) 호출 직전에 GetAuthTicketForWebApi로 새 토큰을 발급받아 파라미터에 넣어야 해요. 부팅 시퀀스마다 새로 발급하면 문제없어요.
결과 코드(49500·403201·406401·503100 등)로 분기하려는데 GetResultCode() 값이 예상과 달라요백엔드 응답 코드는 GetResultCode()가 아니라 GetExternalError()로 전달돼요. GetResultCode()는 SDK가 분류한 결과 코드예요.별도의 레퍼런스 문서결과 코드 표를 기준으로 분기할 때는 cb->GetExternalError()(C#은 callbackResult.ExternalError)를 사용하세요. 성공 여부 자체는 GetResult()->IsSuccessful()로 판정해요.
콜백이 한참 호출되지 않아요게임 루프에서 Stove_APIModule_RunCallback()이 호출되지 않으면 비동기 결과를 게임에 전달할 수 없어요.메인 루프에서 매 프레임 또는 일정 주기로 Stove_APIModule_RunCallback()을 호출해야 해요. 입력 처리와 렌더 사이에 한 번 호출하면 문제없어요.
32-bit 빌드에서 콜백 진입 직후 스택이 깨져요콜백 호출 규약이 헤더 typedef(__cdecl)과 달라요. /Gz(stdcall 기본) 빌드에서 규약이 어긋나면 스택이 손상돼요.콜백 함수를 void __cdecl OnXxx(...) 형태로 선언해 헤더 typedef과 규약을 맞추세요. C#은 CallingConvention.Cdecl로 이미 맞춰져 있어요.
콜백에서 받은 문자열(토큰·닉네임 등)이 나중에 깨져 있어요콜백으로 전달되는 결과 객체는 모듈 소유라 콜백이 반환되면 메모리에서 사라져요. 포인터를 밖에 보관하면 무효 참조가 돼요.필요한 값은 콜백 안에서 별도 변수로 복사(문자열은 깊은 복사)해 두세요. 결과 객체 자체는 Destroy()하지 마세요.
파라미터 객체를 만들었는데 메모리가 계속 늘어나요Stove_APIModule_CreateParam으로 만든 파라미터 객체를 정리하지 않았어요."만들기 → 채우기 → 호출 → 정리하기"를 한 묶음으로 처리하세요. 비동기 함수는 호출 시점에 값을 복사하므로, 호출 직후 Destroy()해도 안전해요.
약관에 동의했는데도 게임 진입 체크가 다시 406401을 줘요동의 제출(AgreeToGameTermsForSteam)이 실패했는데 결과를 확인하지 않고 게임 진입 체크를 재호출했어요.동의 제출 콜백에서 GetResult()->IsSuccessful()(C#은 Result.IsSuccessful)로 성공을 먼저 확인하고, 성공했을 때만 게임 진입 체크를 재호출하세요.

406401은 실패 종료 코드가 아니에요.

약관 동의 동선으로 들어가라는 신호예요. 사용자가 동의하면 게임 진입 체크를 다시 호출해 이어가면 돼요.

샘플 코드

스팀 초기화부터 PCSDK3 기동까지 이어지는 전체 흐름 예제예요. 모듈·PCSDK3 함수를 제외한 추가 처리는 주석으로 설명했어요.

예제에서 Stove_ 로 시작하지 않는 함수는 SDK 가 제공하지 않는 가상 함수예요.

ShowNoticeUI() · ShowGameTermsUI() · QuitGame() · IsGameRunning() 처럼 화면과 게임 루프에 해당하는 함수는 예제를 설명하기 위해 이름만 정해 둔 것이에요. 실제 구현은 개발사가 게임에 맞게 직접 작성해야 해요. 외부플랫폼연동모듈이 제공하는 함수는 모두 Stove_APIModule_ 로 시작하고, PCSDK3 함수는 Base_ 로 시작해요. 스팀웍스 SDK 함수(GetAuthTicketForWebApi() 등)는 밸브가 제공하는 함수예요.

cpp
// 외부플랫폼연동모듈 C API 예제. C++에서는 CreateParam이 돌려준 포인터의
// vtable 메서드(param->SetGameId 등)를 직접 호출해요.
// (C에서는 api_module_flat.h의 Stove_IModuleXxx_SetYyy 평면 접근자를 사용해요.)
#include "api_module.h"
#include "api_module_types.h"
#include <cwchar>
#include <string>
#include <vector>

extern const wchar_t* g_GameId;
extern const wchar_t* g_SteamSessionToken; // 매번 새로 발급
extern const wchar_t* g_SteamAppId;
extern const wchar_t* g_SteamUserId;

void StartPcsdk();            // PCSDK3 기동 (7단계, PCSDK3 문서 참고)

// 개발사가 구현하는 화면·게임 함수
void ShowNoticeUI(const std::wstring& title, const std::wstring& body);  // 확인 시 게임 종료
void QuitGame();
std::wstring FormatLocalDate(int64_t unixMilliseconds);

// 개발사 화면 모델 예시 — 콜백 밖에서 쓰려면 값을 복사해 둬요.
struct TermsSection
{
    std::wstring title;
    std::wstring text;
    int64_t      enforcedDt = 0;
    bool         mustAgree  = false;   // AgreeType == L"FIRST_MUST"
};

static std::vector<TermsSection> g_TermsSections;
void ShowGameTermsUI(const std::vector<TermsSection>& sections);

// 콜백 규약은 반드시 __cdecl
void __cdecl OnInit(const IModuleAPICallbackResult* cb);
void __cdecl OnGameChecker(const IModuleAPICallbackResult* cb,
                           const IModuleGameCheckerForSteamOutcome* outcome);
void __cdecl OnFetchTerms(const IModuleAPICallbackResult* cb,
                          const IModuleFetchGameTermsForSteamOutcome* outcome);
void __cdecl OnAgreeTerms(const IModuleAPICallbackResult* cb,
                          const IModuleAgreeToGameTermsForSteamOutcome* outcome);

// 게임 진입 체크 요청 (만들기 → 채우기 → 호출 → 정리)
void RequestGameChecker()
{
    auto* param = static_cast<IModuleGameCheckerForSteamParam*>(
        Stove_APIModule_CreateParam(k_EStoveAPIModuleTypeKind_GameCheckerForSteamParam));
    if (param == nullptr) return;

    param->SetGameId(g_GameId);
    param->SetSteamSessionToken(g_SteamSessionToken);

    Stove_APIModule_GameCheckerForSteam(param, OnGameChecker, nullptr);
    param->Destroy();
}

// 약관 조회 요청 (AgType은 스팀 게임서비스 약관 = 1 Steam 고정)
void RequestFetchTerms()
{
    auto* param = static_cast<IModuleFetchGameTermsForSteamParam*>(
        Stove_APIModule_CreateParam(k_EStoveAPIModuleTypeKind_FetchGameTermsForSteamParam));
    if (param == nullptr) return;

    param->SetGameId(g_GameId);
    param->SetAgType(k_EStoveFetchGameTermsForSteamAgType_Steam);

    Stove_APIModule_FetchGameTermsForSteam(param, OnFetchTerms, nullptr);
    param->Destroy();
}

void __cdecl OnInit(const IModuleAPICallbackResult* cb)
{
    const IModuleAPIResult* result = (cb != nullptr) ? cb->GetResult() : nullptr;
    if (result == nullptr || !result->IsSuccessful())
    {
        // 초기화 실패 안내 (개발사 UI)
        return;
    }
    RequestGameChecker();
}

void __cdecl OnGameChecker(const IModuleAPICallbackResult* cb,
                           const IModuleGameCheckerForSteamOutcome* outcome)
{
    const IModuleAPIResult* result = (cb != nullptr) ? cb->GetResult() : nullptr;

    // 성공 여부는 IsSuccessful()로 판정
    if (result != nullptr && result->IsSuccessful())
    {
        // outcome->GetAccessToken() 등 확보 → PCSDK3 기동 (7단계)
        StartPcsdk();
        return;
    }

    // 분기 코드는 GetExternalError()로 (GetResultCode() 아님)
    const int32_t externalError = (cb != nullptr) ? cb->GetExternalError() : 0;
    switch (externalError)
    {
    case k_EStoveGameCheckerForSteamResultCode_NotAgreeTerms: // 406401
        // 이 콜백에는 약관 본문이 없어요. 약관 조회를 한 번 더 호출해요.
        RequestFetchTerms();
        break;

    case k_EStoveGameCheckerForSteamResultCode_GameRestrict: // 403201
    {
        // 제재 안내 문구는 결과 객체가 내려줘요.
        const IModuleGameCheckerForSteamRestrictInfo* info =
            (outcome != nullptr) ? outcome->GetRestrictInfo() : nullptr;
        if (info != nullptr)
        {
            std::wstring title = info->GetBanTypeLabel();
            std::wstring body  = info->GetBlockReasonComment();
            body += L"\n제재 기간 ";
            body += FormatLocalDate(info->GetStartDt());
            body += L" ~ ";
            body += FormatLocalDate(info->GetEndDt());

            ShowNoticeUI(title, body);
        }
        else
        {
            ShowNoticeUI(L"게임 이용 제한", L"게임을 이용할 수 없습니다. 고객센터에 문의해 주세요.");
        }
        break;
    }
    case k_EStoveGameCheckerForSteamResultCode_GameServerMaintenance: // 503100
    {
        // 점검 안내 문구는 결과 객체가 내려줘요.
        const IModuleGameCheckerForSteamMaintenanceInfo* info =
            (outcome != nullptr) ? outcome->GetMaintenanceInfo() : nullptr;
        if (info != nullptr)
        {
            std::wstring title = info->GetTitle();
            std::wstring body  = info->GetMsg();
            body += L"\n점검 시간 ";
            body += FormatLocalDate(info->GetStartDt());
            body += L" ~ ";
            body += FormatLocalDate(info->GetEndDt());

            ShowNoticeUI(title, body);
        }
        else
        {
            ShowNoticeUI(L"서버 점검", L"현재 서버 점검 중입니다. 잠시 후 다시 시도해 주세요.");
        }
        break;
    }
    default:
        // 49500/400000/401000/404xxx/500xxx 등: 부가 정보가 없으므로 문구는 개발사가 정해요.
        ShowNoticeUI(L"접속 오류", L"일시적인 오류가 발생하였습니다. 잠시 후 다시 시도해 주세요.");
        break;
    }
}

void __cdecl OnFetchTerms(const IModuleAPICallbackResult* cb,
                          const IModuleFetchGameTermsForSteamOutcome* outcome)
{
    const IModuleAPIResult* result = (cb != nullptr) ? cb->GetResult() : nullptr;
    if (result == nullptr || !result->IsSuccessful() || outcome == nullptr)
    {
        ShowNoticeUI(L"약관 조회 실패", L"서비스 이용 약관 정보를 불러오지 못했습니다.");
        return;
    }

    // 콜백이 반환되면 포인터가 무효가 되므로, 화면 모델로 값을 복사해요.
    g_TermsSections.clear();

    const uint32_t count = outcome->GetContentCount();
    for (uint32_t i = 0; i < count; ++i)
    {
        const IModuleFetchGameTermsForSteamContent* c = outcome->GetContentAt(i);
        if (c == nullptr) continue;

        TermsSection section;
        section.title      = (c->GetTitle() != nullptr) ? c->GetTitle() : L"";
        section.text       = (c->GetText()  != nullptr) ? c->GetText()  : L"";
        section.enforcedDt = c->GetEnforcedDt();

        // 필수 여부는 배열 순서가 아니라 AgreeType으로 판단해요.
        const wchar_t* agreeType = c->GetAgreeType(); // "FIRST_MUST" / "NONE"
        section.mustAgree = (agreeType != nullptr && wcscmp(agreeType, L"FIRST_MUST") == 0);

        g_TermsSections.push_back(std::move(section));
    }

    // 화면을 띄워요. 동의 결과는 아래 OnUserAgreed / OnUserDeclined로 돌아와요.
    ShowGameTermsUI(g_TermsSections);
}

// 사용자가 동의를 누르면 개발사 UI가 호출해요.
void OnUserAgreed()
{
    auto* param = static_cast<IModuleAgreeToGameTermsForSteamParam*>(
        Stove_APIModule_CreateParam(k_EStoveAPIModuleTypeKind_AgreeToGameTermsForSteamParam));
    if (param == nullptr) return;

    // 항목별 동의 값은 보내지 않아요. 게임 단위로 한 번만 제출해요.
    param->SetGameId(g_GameId);
    param->SetSteamSessionToken(g_SteamSessionToken);

    Stove_APIModule_AgreeToGameTermsForSteam(param, OnAgreeTerms, nullptr);
    param->Destroy();
}

// 동의하지 않으면 게임에 진입할 수 없어요.
void OnUserDeclined()
{
    QuitGame();
}

void __cdecl OnAgreeTerms(const IModuleAPICallbackResult* cb,
                          const IModuleAgreeToGameTermsForSteamOutcome* outcome)
{
    const IModuleAPIResult* result = (cb != nullptr) ? cb->GetResult() : nullptr;
    if (result == nullptr || !result->IsSuccessful())
    {
        // 실패한 상태로 재호출하면 406401이 반복돼요.
        ShowNoticeUI(L"약관 동의 실패", L"약관 동의 처리에 실패하였습니다. 잠시 후 다시 시도해 주세요.");
        return;
    }

    const wchar_t* guid = (outcome != nullptr) ? outcome->GetGuid() : nullptr;
    (void)guid;

    // guid는 별도 처리 없이, 곧바로 게임 진입 체크를 재호출해요
    RequestGameChecker();
}

void GameMain()
{
    // 1) 스팀 세션 토큰 발급 (매번 새로)
    // g_SteamSessionToken = SteamUser()->GetAuthTicketForWebApi(...);

    // 2) 초기화
    auto* initParam = static_cast<IModuleAPIInitializeParam*>(
        Stove_APIModule_CreateParam(k_EStoveAPIModuleTypeKind_APIInitializeParam));
    if (initParam == nullptr) return;

    initParam->SetEnvironment(L"LIVE");
    initParam->SetPlatformName(L"STEAM");   // 고정값
    initParam->SetSteamAppId(g_SteamAppId);
    initParam->SetSteamUserId(g_SteamUserId);

    Stove_APIModule_Initialize(initParam, OnInit, nullptr);
    initParam->Destroy();

    // 3) 콜백 펌프 (게임 메인 스레드)
    while (IsGameRunning())
    {
        Stove_APIModule_RunCallback();
        // PCSDK3 기동 후에는 Base_RunCallback()도 함께 호출
        // ... 게임 프레임 처리 ...
    }

    // 4) 정리 (동기 반환 객체는 호출자가 Destroy)
    IModuleAPIResult* unInit = Stove_APIModule_UnInitialize();
    if (unInit != nullptr) unInit->Destroy();
}

StartPcsdk / Base_RestartAppIfNecessaryAsync / Base_InitializeEx는 PCSDK3(BaseSDK) 영역이에요.

자세한 사용법은 PCSDK3 문서를 참고하세요. 게임 진입 체크로 확보한 접속 정보는 모듈이 내부 IPC로 PCSDK3에 전달하므로, 개발사가 토큰을 직접 넘길 필요는 없어요.

자주 묻는 질문

Q1. 이 모듈은 모든 게임이 적용해야 하나요?
A. 아니에요. 외부플랫폼연동모듈은 선택 적용 항목입니다. 게임을 스팀런처에서도 배포·실행하면서 스토브 플랫폼 기능을 사용하려는 경우에만 연동하면 돼요.
스토브런처로만 배포한다면 이 모듈 없이 PCSDK3만 연동하면 충분해요.

Q2. SDK 호출 순서는 어떻게 되나요?
A. 게임 시작 시에는 스팀 세션 토큰 발급 → Stove_APIModule_Initialize → Stove_APIModule_GameCheckerForSteam 순서예요.
성공하면 이어서 PCSDK3 기동(Base_RestartAppIfNecessaryAsync → Base_InitializeEx)으로 넘어가고, 406401이면 약관 조회 → 동의 → 게임 진입 체크 재호출로 이어가요.
게임 종료 시에는 Stove_APIModule_UnInitialize로 정리해요.

Q3. 결과 코드로 분기하려는데 어떤 접근자를 써야 하나요?
A. 성공 여부는 GetResult()->IsSuccessful()로 판정해요.
49500·403201·406401·503100 같은 백엔드 코드는 GetResultCode()가 아니라 GetExternalError()(C#은 ExternalError)로 전달돼요. 별도의 레퍼런스 문서의 결과 코드 표를 기준으로 분기할 때는 반드시 GetExternalError()를 사용하세요.

Q4. 스팀 세션 토큰은 캐시해 두고 재사용해도 되나요?
A. 안 돼요. ISteamUser::GetAuthTicketForWebApi로 발급받는 토큰은 매번 새로 얻는 값이에요.
초기화나 게임 진입 체크를 호출하기 직전에 새로 발급받은 값을 넘겨야 하며, 예전 값을 재사용하면 서버가 거절해 BadRequest(400000) 등이 발생할 수 있어요.

Q5. 콜백에서 받은 토큰·문자열을 나중에 써도 되나요?
A. 콜백으로 전달되는 결과 객체는 모듈 소유라 콜백이 반환되면 메모리에서 사라져요.
필요한 값은 콜백 안에서 별도 변수로 복사(문자열은 깊은 복사)해 두고 쓰세요. 결과 객체 자체는 Destroy()하지 말아야 해요. 반대로 파라미터 객체와 동기 함수 반환 객체는 호출자가 정리해요.

Q6. 콜백 펌프는 아무 스레드에서 호출해도 되나요?
A. Stove_APIModule_RunCallback()은 반드시 게임 메인 스레드에서 호출해야 해요. 콜백은 펌프를 호출한 스레드에서 실행되므로, 메인 스레드에서 펌프를 돌리면 게임 UI·로직과 같은 스레드에서 콜백이 실행돼 동기화 부담을 줄일 수 있어요.

Q7. 약관 화면은 SDK가 그려 주나요?
A. 화면은 개발사가 그리지만, 화면에 넣을 약관 문구는 개발사가 작성하지 않아요. 스토브 파트너스에 등록된 약관의 제목과 본문을 약관 조회 API가 그대로 내려주니, 받은 값을 화면에 표시하기만 하면 돼요. 개발사가 정하는 것은 화면 제목, 체크박스 라벨, 버튼 라벨, 날짜 표기 형식이에요.
본문은 HTML 로 전달되니 받은 값을 그대로 표시하고, 줄바꿈을 살려 스크롤 영역에 넣어 주세요. HTML 을 그대로 표시하기 어렵다면 평문으로 바꿔 받을 수 있으니 기술지원으로 문의해 주세요.
약관이 여러 건 내려올 수 있으니 전부 보여줘야 하고, 동의 체크는 항목마다 하나씩 두면 돼요. 동의 버튼을 누르면 위 항목이 모두 체크되고 동의가 제출돼요. 유형 구분은 배열 순서가 아니라 GetAgreeType() 값(FIRST_MUST/NONE)으로 하세요.

Q8. 인증에 성공하면 액세스 토큰을 PCSDK3에 직접 넘겨야 하나요?
A. 아니에요. 게임 진입 체크로 확보한 접속 정보는 외부플랫폼연동모듈이 내부 IPC로 PCSDK3에 전달해요.
개발사는 Base_RestartAppIfNecessaryAsync → Base_InitializeEx 순서로 PCSDK3 기동 함수만 호출하면 돼요. 초기화 방식은 기존 스토브 런처 기동과 동일하므로 PCSDK3 문서를 참고하세요.

Q9. 32-bit 빌드에서 콜백 진입 직후 크래시가 나요.
A. 콜백 호출 규약이 헤더 typedef(__cdecl)과 다르면 /Gz(stdcall 기본) 빌드에서 스택이 손상돼요.
콜백 함수를 void __cdecl OnXxx(...) 형태로 선언해 규약을 맞추세요. C#은 CallingConvention.Cdecl로 이미 맞춰져 있어요.

Q10. Shadow 계정과 정회원을 게임에서 따로 처리해야 하나요?
A. 아니에요. SDK·게임은 두 유형을 구분하지 않고 동일하게 동작해요. 게임 진입 체크 동선에서 계정 유형을 분기할 필요가 없어요.
신규 스팀 유저(스토브 회원 없음)가 약관 동의를 제출하면 스토브 백엔드가 Shadow 계정 생성을 완료하고 guid를 발급해요. 모듈에는 Shadow 생성 로직이 없어요. 정회원 전환은 스토브 웹에서 진행되며, 이 모듈은 전환 전용 API를 제공하지 않아요.

Q11. 사용자가 약관에 동의하지 않으면 어떻게 하나요?
A. 게임에 진입할 수 없어요. 안내 후 게임을 종료하세요.
동의는 게임 단위로 한 번 제출하며, 어떤 항목에 동의했는지는 서버로 보내지 않아요. 동의 제출 API에 넘기는 값은 게임 아이디와 스팀 세션 토큰뿐이에요.
제출이 성공한 뒤에 게임 진입 체크를 다시 호출하면 정상 진입돼요. 성공을 확인하지 않고 재호출하면 406401이 반복돼요.



직접 문의하고 싶으신가요? stove.developers@smilegate.com