안티치트
Connect Base 게임 서버는 서버 권한 모델을 사용하여 치트를 방지합니다. 모든 게임 로직은 서버에서 검증됩니다.
서버 권한 모델
클라이언트 A 클라이언트 B
│ │
▼ ▼
액션 전송 액션 전송
│ │
└──────┬───────┘
▼
┌───────────┐
│ 게임 서버 │ ◀── 검증 및 상태 계산
└───────────┘
│
▼
상태 브로드캐스트
│
┌──────┴───────┐
▼ ▼
클라이언트 A 클라이언트 B액션 검증
서버에서 모든 액션을 검증합니다:
typescript
// 서버 측 (Lua 스크립트)
function validateAction(playerId, action, currentState)
-- 이동 속도 검증
if action.type == "move" then
local player = currentState.players[playerId]
local distance = math.sqrt(
(action.data.x - player.x)^2 +
(action.data.y - player.y)^2
)
local maxSpeed = 10 -- 최대 이동 속도
local deltaTime = action.timestamp - player.lastMoveTime
local maxDistance = maxSpeed * deltaTime
if distance > maxDistance * 1.1 then -- 10% 오차 허용
return false, "speed_hack_detected"
end
end
-- 쿨다운 검증
if action.type == "use_skill" then
local skill = skills[action.data.skillId]
local lastUse = currentState.skillCooldowns[playerId][action.data.skillId]
if lastUse and (action.timestamp - lastUse) < skill.cooldown then
return false, "cooldown_not_ready"
end
end
return true
end레이트 리미팅
초당 액션 수를 제한합니다:
typescript
// 서버 측 설정
const RATE_LIMITS = {
move: 60, // 초당 60회 이동
attack: 10, // 초당 10회 공격
chat: 3, // 초당 3회 채팅
}
// 클라이언트 측에서도 제한
class RateLimiter {
private counts: Map<string, number[]> = new Map()
canPerform(actionType: string): boolean {
const limit = RATE_LIMITS[actionType] || 10
const now = Date.now()
const windowMs = 1000
const timestamps = this.counts.get(actionType) || []
const recentTimestamps = timestamps.filter(t => now - t < windowMs)
if (recentTimestamps.length >= limit) {
return false
}
recentTimestamps.push(now)
this.counts.set(actionType, recentTimestamps)
return true
}
}
const rateLimiter = new RateLimiter()
function sendAction(action: GameAction) {
if (!rateLimiter.canPerform(action.type)) {
console.warn('Rate limit exceeded')
return
}
game.sendAction(action)
}시퀀스 검증
액션 순서를 검증합니다:
typescript
// 클라이언트
let actionSequence = 0
function sendAction(action: GameAction) {
game.sendAction({
...action,
sequence: actionSequence++
})
}
// 서버 측
function validateSequence(playerId, action, lastSequence)
if action.sequence <= lastSequence then
-- 중복 또는 순서 오류
return false, "invalid_sequence"
end
if action.sequence > lastSequence + 100 then
-- 너무 큰 점프 (의심스러움)
return false, "sequence_gap_too_large"
end
return true
end히트박스 검증
서버에서 충돌 판정:
typescript
// 서버 측 히트 판정
function validateHit(attacker, target, weapon)
local distance = getDistance(attacker.position, target.position)
-- 무기 사거리 확인
if distance > weapon.range * 1.1 then
return false, "out_of_range"
end
-- 시야선 확인 (벽 뒤 공격 방지)
if not hasLineOfSight(attacker.position, target.position) then
return false, "no_line_of_sight"
end
-- 히트박스 교차 확인
if not intersectsHitbox(weapon.hitbox, target.hitbox) then
return false, "hitbox_miss"
end
return true
end비정상 행동 감지
통계 기반 이상 탐지:
typescript
// 서버 측 통계 수집
const playerStats = new Map<string, PlayerStats>()
function recordAction(playerId: string, action: GameAction) {
const stats = playerStats.get(playerId) || new PlayerStats()
stats.actionCount++
stats.recordAccuracy(action)
stats.recordReactionTime(action)
// 비정상 탐지
if (stats.accuracy > 0.99 && stats.actionCount > 100) {
flagForReview(playerId, 'suspiciously_high_accuracy')
}
if (stats.avgReactionTime < 50) { // 50ms 미만 반응
flagForReview(playerId, 'inhuman_reaction_time')
}
}리플레이 시스템
의심스러운 플레이를 검토하기 위한 리플레이 저장:
typescript
// 게임 세션 기록
interface GameReplay {
roomId: string
startTime: number
endTime: number
players: string[]
events: ReplayEvent[]
}
interface ReplayEvent {
timestamp: number
type: 'action' | 'state' | 'player_event'
data: unknown
}
// 서버에서 리플레이 저장
async function saveReplay(replay: GameReplay) {
await cb.database.createData('tbl_replays', { data: replay })
}
// 리플레이 재생
async function playReplay(replayId: string) {
const row = await cb.database.getDataById('tbl_replays', replayId)
const replay = row.data as unknown as GameReplay
for (const event of replay.events) {
await delay(event.timestamp - replay.startTime)
renderEvent(event)
}
}