Repository navigation
GDScript SDK
Godot 4.3 앱용 addon. 현재 권장 버전은 0.6.7입니다.
- source:
sdk-gdscript - 버전 정본:
sdk-gdscript/VERSION - 배포본: GitHub Release
v0.6.7의seorilabs-platform-gdscript-0.6.7.tar.gz - 전체 이력: GDScript SDK 버전 이력
0.6.5는 철회됐습니다.0.6.4이하에서 올릴 때는0.6.7로 바로 갑니다.
GDScript에는 패키지 매니저가 없습니다. Release tarball을 받아 sha256을 검증하고
addons/ 아래에 풉니다.
V=0.6.7
B=https://github.com/seorilabs/platform/releases/download/v$V
curl -fsSLO "$B/seorilabs-platform-gdscript-$V.tar.gz"
curl -fsSLO "$B/seorilabs-platform-gdscript-$V.tar.gz.sha256"
sha256sum -c "seorilabs-platform-gdscript-$V.tar.gz.sha256" # macOS: shasum -a 256 -c
tar -xzf "seorilabs-platform-gdscript-$V.tar.gz" -C addons/풀면 addons/seorilabs_platform/이 생깁니다.
addons/seorilabs_platform/
platform_client.gd 진입점. class_name SeoriPlatformClient
core/ 전송, envelope, 백오프, 정규화, presence
adapters/ Firebase identity, AdMob rewarded claim 표준 어댑터
tools/ SDK 자체 검증. 지워도 된다
SOURCE VERSION CHECKSUM 출처 고정. addon과 같은 커밋에 넣는다
SOURCE는 다운로드한 Release asset URL을 가리킵니다. VERSION만 같고 CHECKSUM이
다르면 같은 배포본이 아닙니다 — 파일을 직접 고치지 말고, 올릴 때는 새 tarball로
통째로 바꿉니다.
symlink나 런타임 네트워크 의존으로 연결하지 않습니다. 모바일 export와 격리된 CI checkout에서도 같은 코드가 들어가야 합니다.
var platform := SeoriPlatformClient.new()
platform.configure({
"base_url": PLATFORM_API_URL,
"iap_base_url": PLATFORM_IAP_URL,
"ingest_base_url": PLATFORM_INGEST_URL,
"ads_base_url": PLATFORM_ADS_URL,
"app_id": "my-app",
"event_context": func():
return {
"platform": "android",
"appVersion": ProjectSettings.get_setting("application/config/version"),
"locale": TranslationServer.get_locale(),
},
"presence_enabled": false,
})
add_child(platform) # HTTPRequest가 동작하려면 트리에 있어야 한다| 키 | 기본 | 의미 |
|---|---|---|
base_url |
필수 |
api 역할 URL |
app_id |
필수 | 레지스트리의 app_id
|
iap_base_url ingest_base_url ads_base_url
|
base_url |
역할별 URL. production은 전부 지정 |
auth_base_url |
base_url |
세션 발급 host. AppsInToss처럼 mTLS 자격증명이 iap에 격리된 앱만 지정 |
event_context |
{} |
Dictionary 또는 Callable. Callable은 flush 시점에 평가 — 언어가 바뀌어도 다음 배치부터 반영 |
presence_enabled |
false |
RPI edge heartbeat. 명시적으로 켠 앱만 |
max_retries |
SDK 기본 | 전송 계층 재시도 |
event_context에는 OpenAPI에 선언된 platform, appVersion, locale,
ga4ClientId만 들어갑니다. sdkVersion은 배포본 버전으로 SDK가 고정합니다.
모든 콜백은 Dictionary 하나를 받습니다.
| 키 | 뜻 |
|---|---|
ok |
성공 여부 |
result |
성공 결과 |
code |
오류 코드. 분기는 이 값으로만 → Error Codes |
message |
사람이 읽는 메시지. 분기에 쓰지 않는다 |
local |
로컬 판정인가. 서버에 닿지 못한 경우 |
http_status |
HTTP 상태 |
응답의 모르는 필드는 무시합니다. 키 개수를 세지 않습니다 — 서버가 필드 하나를 추가해도 마켓에 배포된 구버전이 깨지면 안 됩니다.
platform.sign_in({"kind": "firebase-id-token", "value": id_token},
func(res: Dictionary) -> void:
if not res["ok"]:
push_warning("로그인 실패: %s" % res["code"])
return
_on_signed_in(res["result"])
)| kind | value | 결제 |
|---|---|---|
firebase-id-token |
앱 Firebase Auth의 ID 토큰 | 가능 |
ait-login |
AppsInToss appLogin() 결과 |
가능 |
anonymous |
getAnonymousKey() 해시 |
불가 — 사칭 가능 |
sign_in은 자격증명을 보관해 refresh가 실패했을 때 다시 로그인합니다. Godot에서
매번 Firebase 토큰을 다시 받아오게 하면 호출 앞에 왕복이 두 번씩 붙기 때문입니다.
| 조회 | |
|---|---|
current_session() |
세션 Dictionary. expiresAt은 epoch ms |
is_signed_in() is_anonymous()
|
|
session_changed 시그널 |
세션이 바뀔 때. 로그아웃이면 빈 Dictionary |
with_token(cb) |
유효한 토큰으로 콜백. 만료 60초 전부터 선제 refresh |
sequenceDiagram
autonumber
participant App as 앱
participant FB as Firebase Auth
participant SDK as Platform SDK
participant API as platform-api
participant FS as Firestore
App->>FB: 로그인
FB-->>App: ID 토큰
App->>SDK: sign_in kind firebase-id-token
SDK->>API: POST /v1/auth/session — X-Seori-App 헤더
API->>API: 레지스트리에서 앱 조회 · paused면 403
API->>API: ID 토큰 서명 · aud · iss · exp 검증
API->>FS: 차단 목록 확인
API->>FS: app_id + uid → platform_user_id 매핑 · 없으면 생성
API-->>SDK: platformToken · refreshToken · supportCode · config
SDK->>SDK: 세션과 자격증명 보관 · session_changed 발신
SDK-->>App: 콜백 — ok · result
sequenceDiagram
participant App as 앱
participant SDK as Platform SDK
participant API as platform-api
App->>SDK: with_token 또는 인증이 필요한 호출
SDK->>SDK: expiresAt까지 60초 미만인가
alt 아직 유효
SDK-->>App: 저장된 platformToken으로 진행
else 만료 임박 — 동시 호출도 refresh는 한 번
SDK->>API: POST /v1/auth/refresh — refreshToken
alt 200
API-->>SDK: 새 세션
SDK->>SDK: 저장 후 진행
else 401 또는 403
SDK->>API: 보관한 자격증명으로 POST /v1/auth/session 재로그인
Note over SDK: IAP 경로는 첫 401 session_expired에만<br/>refresh 후 한 번 replay하고 재로그인하지 않는다
end
end
platform.create_firebase_custom_token("", app_check_token, func(bridge):
if not bridge["ok"]:
return
# bridge.result.firebaseCustomToken → Firebase signInWithCustomToken에 한 번 쓰고 저장하지 않는다
# 그 뒤 ID 토큰으로 sign_in
)첫 인자에 기존 Firebase ID 토큰을 넣으면 uid를 보존합니다. 신규 발급은 자동 재시도하지 않습니다 — 응답 유실 뒤 재시도하면 다른 uid가 생길 수 있습니다.
이 흐름 전체를 adapters/firebase_identity_adapter.gd가 표준으로 구현합니다.
직접 Firebase 익명 가입으로 우회하지 않고, 기존 UID 이전이 실패하면 fail-closed합니다.
UID와 refresh token만 원자적으로 저장하고 Custom Token과 ID token은 메모리에만 둡니다.
const FirebaseIdentity := preload("res://addons/seorilabs_platform/adapters/firebase_identity_adapter.gd")
var identity := FirebaseIdentity.new()
add_child(identity)
identity.configure({
"firebase_api_key": firebase_api_key,
"platform_client": platform,
"state_path": "user://firebase_auth.json",
})
var who: Dictionary = await identity.ensure_identity()어댑터가 하는 일 전체입니다. 저장된 신원이 있으면 Firebase refresh token으로 바로 가고, 처음이면 플랫폼 custom token을 거칩니다.
sequenceDiagram
autonumber
participant App as 게임
participant AD as firebase_identity_adapter
participant SDK as SeoriPlatformClient
participant API as platform-api
participant FB as Firebase Auth REST
App->>AD: ensure_identity
AD->>AD: user 저장소에서 uid · refresh token 로드
alt 저장된 신원이 있다
AD->>FB: refresh token으로 ID 토큰 갱신
else 처음이다
AD->>SDK: create_firebase_custom_token — App Check 토큰
SDK->>API: POST /v1/auth/firebase-custom-token — no_retry
API-->>SDK: firebaseCustomToken · appUserId
SDK-->>AD: 콜백
AD->>FB: signInWithCustomToken
FB-->>AD: ID 토큰 · refresh token
AD->>AD: uid와 refresh token만 원자적으로 저장
end
AD->>SDK: sign_in kind firebase-id-token
SDK->>API: POST /v1/auth/session
API-->>SDK: 세션
AD-->>App: identity — 실패하면 fail-closed
Firebase ID 토큰이 없습니다. appLogin() 결과를 ait-login으로 넘기면 서버가 Toss
Login API와 mTLS로 교환합니다. 이 자격증명은 iap·ads 역할에만 있으므로
auth_base_url을 그쪽으로 지정합니다.
sequenceDiagram
autonumber
participant App as 앱 — AppsInToss
participant AIT as AppsInToss SDK
participant SDK as Platform SDK
participant IAP as platform-iap 또는 platform-ads
participant TOSS as Toss Login API
App->>AIT: appLogin
AIT-->>App: authorization code · referrer
App->>SDK: sign_in kind ait-login · referrer
SDK->>IAP: POST /v1/auth/session
IAP->>TOSS: mTLS로 code 교환
TOSS-->>IAP: 사용자 식별자
IAP->>IAP: 앱 범위 platform_user 매핑
IAP-->>SDK: 세션 — isLinkedAccount true
SDK-->>App: 콜백 — ok · result
Note over SDK,IAP: mTLS 자격증명이 iap · ads 역할에만 있어<br/>세션 발급 host를 그쪽으로 지정한다
platform.track("level_complete", {"level": 3, "is_new": true})
platform.flush_events() # 백그라운드 진입, 종료 전- 즉시 보내지 않고 모았다가 배치로 보냅니다
- 실패한 배치는 outbox에 남아 다시 시도합니다
- 던지지 않습니다. 계측이 게임을 멈추면 안 됩니다
-
pending_event_count()로 남은 수를 봅니다
앱이 이미 canonical envelope(eventId, 발생 시각)를 갖고 있으면 track_event(event)로
그대로 보존합니다.
파라미터 정규화는 TypeScript SDK와 같은 출력을 냅니다 —
spec/conformance/param-normalization.json이
정본입니다. boolean은 1/0, 객체·배열은 버리고, PII 키는 버립니다.
GA4를 대체하지 않고 병행합니다. GA4는 마케팅·잔존율, 플랫폼은 운영·CS·결제 근거입니다. 이벤트명을 한쪽만 바꾸지 않습니다.
sequenceDiagram
participant App as 앱
participant SDK as Platform SDK — Events
participant OB as Outbox
participant ING as platform-ingest
App->>SDK: track name · params
SDK->>SDK: allowlist 확인 · 파라미터 정규화 · eventId 부여
SDK->>SDK: 버퍼에 추가 — 던지지 않는다
alt 배치 도달 · 주기 · flush_events 호출
SDK->>OB: 이전 실패분 drain
SDK->>ING: POST /v1/events — 최대 20건 · 세션 토큰은 있으면
alt 성공
ING-->>SDK: ok
else 실패
SDK->>OB: 배치 보관 — 최대 500건
Note over OB: 다음 flush에 다시 시도
end
end
platform.verify_purchase(
{"platform": "google_play", "product_id": "gecko_galaxy", "token": purchase_token},
func(res: Dictionary) -> void:
if not res["ok"]:
_show_purchase_error(res["code"])
return
var result: Dictionary = res["result"]
_apply_entitlements(result["entitlements"])
var completion: Dictionary = result.get("completion", {})
if String(completion.get("action", "none")) == "apps_in_toss_complete_product_grant":
_ait_complete_product_grant(String(completion.get("orderId", "")))
)| 마켓 | token |
|---|---|
| Google Play | purchaseToken |
| App Store | transactionId |
| AppsInToss | orderId |
verify_purchase, list_entitlements, account_references는 전송 계층의 일반
재시도를 모두 끕니다. 첫 응답이 정확히 401 session_expired일 때만 refresh를 한 번
요청하고 새 토큰으로 원 요청을 한 번 replay합니다. refresh 실패, 두 번째 401, 403,
5xx, timeout은 그대로 한 번 반환합니다.
익명 세션은 SDK가 서버에 가기 전에 anonymous_not_allowed로 막습니다.
구매 전 계정 참조:
platform.account_references(func(res):
# res.result.googlePlayObfuscatedAccountId / appStoreAppAccountToken
# 마켓 결제 화면에 넣어 다른 사용자의 구매 가로채기를 막는다
)구매 전 계정 참조부터 완료 처리까지의 전체 흐름입니다.
sequenceDiagram
autonumber
participant App as 앱
participant SDK as Platform SDK
participant MK as 마켓 SDK
participant IAP as platform-iap
participant MAPI as 마켓 API
participant FS as 원장 Firestore
App->>SDK: account_references
SDK->>IAP: GET /v1/iap/account-references
IAP-->>SDK: HMAC 계정 참조
App->>MK: 구매 요청 — obfuscatedAccountId · appAccountToken
MK-->>App: purchaseToken · transactionId · orderId
App->>SDK: verify_purchase platform · product_id · token
SDK->>IAP: POST /v1/iap/verify — 일반 재시도 없음
IAP->>IAP: 마켓 검증기 선택 · SKU를 entitlement로 변환
IAP->>MAPI: 구매 확인
MAPI-->>IAP: 거래 상태
IAP->>IAP: 계정 바인딩 검증 — 불일치는 신호로 기록하고 지급은 막지 않는다
IAP->>FS: 트랜잭션 — 지급 · 보류 · 회수
IAP->>MAPI: 완료 처리 — Play consume 등 · 실패해도 지급 유지
IAP-->>SDK: status · entitlements · granted 또는 alreadyGranted · completion
SDK-->>App: 콜백 — ok · result
opt completion.action이 none이 아니면
App->>MK: 마켓 쪽 마무리 — AppsInToss 지급 완료 등
end
환불은 마켓 웹훅으로 서버에 먼저 도착합니다. 앱은 다음 시작 때 list_entitlements로
서버가 아는 최종 상태를 받아 반영합니다.
sequenceDiagram
autonumber
participant MAPI as App Store 또는 Google Play
participant IAP as platform-iap
participant FS as 원장 Firestore
participant App as 앱
participant SDK as Platform SDK
MAPI->>IAP: 환불 알림 — Apple JWS 또는 Play RTDN
IAP->>IAP: 서명 · OIDC 검증
IAP->>FS: 기존 소유자의 entitlement 회수 — 알림만으로 신규 지급은 없다
Note over App,SDK: 다음 앱 시작
App->>SDK: list_entitlements
SDK->>IAP: GET /v1/iap/entitlements
IAP-->>SDK: 서버가 아는 최종 상태
SDK-->>App: entitlements — 환불분 제외
platform.fetch_config({"app_version": "1.2.3", "platform": "android", "locale": "ko-KR"},
func(res):
var config: Dictionary = res["result"]
if config.get("maintenance", {}).get("active", false):
_show_maintenance()
)
platform.current_config() # 마지막 값. 네트워크를 타지 않는다
platform.is_under_maintenance()
platform.is_sdk_blocked()fetch_config는 **항상 ok: true**입니다. 실패하면 마지막 캐시, 그것도 없으면 열린
기본값을 돌려줍니다. 설정 조회 실패가 앱 시작을 막으면 서버 장애가 전체 중단으로
번집니다. config_changed 시그널로 갱신을 받을 수 있습니다.
flowchart TD
A["config 조회"] --> B{"캐시가 TTL 안인가"}
B -->|예| C["캐시 반환"]
B -->|아니오| D["GET /v1/config — appVersion · platform · locale"]
D --> E{"성공"}
E -->|예| F["캐시 갱신 후 반환"]
E -->|아니오| G{"이전 캐시가 있는가"}
G -->|예| H["마지막 캐시 반환"]
G -->|아니오| I["열린 기본값 반환 — 차단 없음"]
F --> J{"sdk.status blocked<br/>또는 maintenance.active"}
H --> J
I --> J
J -->|예| K["앱이 차단 · 점검 화면 표시"]
J -->|아니오| L["정상 진행"]
저수준 API는 get_ads_policy, create_reward_claim, get_reward_claim,
confirm_reward_claim, ack_reward_claim입니다. 게임은 이걸 직접 쓰지 않고
adapters/rewarded_claim_adapter.gd를 씁니다. Custom Token 교환과 claim 상태 전이를
다시 구현하지 않고, 네이티브 AdMob 표시 코드에는 custom_data와 user_id만 넘깁니다.
const RewardedClaims := preload("res://addons/seorilabs_platform/adapters/rewarded_claim_adapter.gd")
var rewards := RewardedClaims.new()
add_child(rewards)
rewards.configure({
"platform_client": platform,
"identity_adapter": identity,
"client_platform": "android",
})
var policy: Dictionary = await rewards.policy()
if not policy.get("allowed", false):
return
var created: Dictionary = await rewards.create_admob_claim({
"request_id": local_claim_id,
"placement": "hint_reward",
"reward_key": "hint",
"reward_amount": 3,
})
var ssv := rewards.ssv_options(local_claim_id)
# 네이티브 SDK의 ServerSideVerificationOptions에 ssv 값을 설정한다
# recover_admob_claim이 server_verified이면 로컬 exactly-once 보상을 정산하고,
# 정산이 끝난 뒤에만 acknowledge한다
await rewards.acknowledge(local_claim_id)
# 광고 미노출·미보상 종료, 또는 recover가 final failed로 끝난 경우에만
rewards.discard_unsettled_claim(local_claim_id)정책 조회 실패를 광고 허용으로 바꾸지 않습니다. pending claim 참조와 ack 재시도는 로컬에 보존합니다. 이미 로컬 정산 후 ack 대기열에 든 claim은 폐기하지 않습니다. 광고 unit, placement, reward 범위는 앱 레지스트리가 원장입니다.
stateDiagram-v2
[*] --> accepted: claim 생성
accepted --> confirmed: AdMob SSV 콜백 — server_verified
accepted --> confirmed: confirm — client_confirmed · AppsInToss만
accepted --> expired: expiresAt 경과
confirmed --> delivered: ack — 로컬 지급 완료 후
delivered --> [*]
expired --> [*]
sequenceDiagram
autonumber
participant App as 게임
participant AD as rewarded_claim_adapter
participant SDK as SeoriPlatformClient
participant ADS as platform-ads
participant GMA as AdMob 네이티브
participant SSV as AdMob SSV
App->>AD: policy
AD->>AD: ensure_session — identity adapter로 로그인 보장
AD->>SDK: get_ads_policy
SDK->>ADS: GET /v1/ads/policy
ADS-->>AD: adsEnabled · disabledBy
alt allowed false 또는 조회 실패
AD-->>App: 광고 중단 — fail-closed
end
App->>AD: create_admob_claim request_id · placement · reward
AD->>SDK: create_reward_claim
SDK->>ADS: POST /v1/ads/reward-claims
ADS-->>AD: claim — accepted · admobSsv
AD->>AD: claim 참조를 로컬에 저장
App->>AD: ssv_options request_id
AD-->>App: custom_data · user_id
App->>GMA: 보상형 광고 표시 — ServerSideVerificationOptions
GMA->>SSV: 시청 완료
SSV->>ADS: GET /v1/ads/admob/ssv/appId — 서명된 콜백
ADS->>ADS: 서명 검증 · 한도 재판정 → confirmed · server_verified
App->>AD: recover_admob_claim request
AD->>SDK: get_reward_claim
SDK->>ADS: GET /v1/ads/reward-claims/claimId
ADS-->>AD: state · assurance
alt confirmed 또는 delivered 이고 server_verified
AD-->>App: verified
App->>App: 로컬 exactly-once 보상 지급
App->>AD: acknowledge request_id
AD->>SDK: ack_reward_claim
SDK->>ADS: POST /v1/ads/reward-claims/claimId/ack
ADS-->>AD: delivered
else expired
AD-->>App: failed — discard_unsettled_claim으로 정리
else 그 외
AD-->>App: pending — 다음 기회에 recover 재호출
end
AppsInToss 광고는 SSV가 없어 앱이 confirm_reward_claim으로 transaction_id를 보고합니다.
sequenceDiagram
autonumber
participant App as 앱 — AppsInToss
participant SDK as Platform SDK
participant ADS as platform-ads
participant AIT as AppsInToss 광고 SDK
App->>SDK: create_reward_claim provider apps_in_toss
SDK->>ADS: POST /v1/ads/reward-claims
ADS-->>SDK: claim — state accepted
App->>AIT: 보상형 광고 표시
AIT-->>App: 보상 완료 · transactionId
App->>SDK: confirm_reward_claim claim_id · transaction_id
SDK->>ADS: POST /v1/ads/reward-claims/claimId/confirm
ADS->>ADS: provider가 apps_in_toss인지 확인 · transactionId 해시 · 한도 재판정
ADS-->>SDK: confirmed · client_confirmed
App->>App: 로컬 exactly-once 보상 지급
App->>SDK: ack_reward_claim claim_id
SDK->>ADS: POST /v1/ads/reward-claims/claimId/ack
ADS-->>SDK: delivered
presence_enabled: true이고 레지스트리에 features.presence가 켜진 앱만 heartbeat를
보냅니다. 제품 요청과 별도 HTTPRequest를 쓰고, edge timeout 2초·token timeout 5초
안에서 끝나며 실패를 인증·결제 흐름에 전달하지 않습니다. Godot focus-in은 자동
감지하고, host가 별도 foreground 이벤트를 주면 resume_presence()를 부릅니다.
sequenceDiagram
participant SDK as Platform SDK — Presence
participant ING as platform-ingest
participant EDGE as presence edge — RPI
SDK->>ING: POST /v1/presence/token — 익명 세션 ID
alt enabled false 또는 발급기 미준비
ING-->>SDK: enabled false — 5분 뒤 다시 bootstrap
else
ING-->>SDK: 단기 토큰 · edgeUrl
loop 60초 주기 · 실패 시 최대 5분 backoff
SDK->>EDGE: POST /v1/presence/heartbeat — sequence · platform · appVersion
EDGE-->>SDK: 200 또는 Retry-After
end
end
Note over SDK: 실패를 던지지도 outbox에 넣지도 않는다
- HTTP 요청은 직렬로 흐릅니다. Godot
HTTPRequest가 한 번에 하나만 처리합니다. 플랫폼 호출은 빈도가 낮아 충분합니다 -
Retry-After는 초 단위 숫자만 읽습니다. Godot에 HTTP-date 파서가 없습니다 - 세션 만료 기준은 monotonic tick이 아니라
expiresAt(epoch ms)입니다. 기기 sleep 중 멈추는 tick은 쓰지 않습니다
sha256sum -c seorilabs-platform-gdscript-0.6.7.tar.gz.sha256 # 가져올 때
godot --headless --script addons/seorilabs_platform/tools/conformance_probe.gd
godot --headless --script addons/seorilabs_platform/tools/smoke_probe.gdexit code만 보면 안 됩니다. Godot은 SCRIPT ERROR가 나도 실행을 계속하므로 로그에서
SCRIPT ERROR와 ERROR:를 함께 찾습니다.
레퍼런스 배선은 examples/reference-godot에 있습니다.
구현과 API 계약의 정본은 seorilabs/platform 저장소입니다.