Bloga Dön

Sunucu Kodu Olmadan Adil Liderlik Tabloları: Doğrulanmış Turlar, Sus Paketleri ve Oyuncu Profilleri

Yayınlanma tarihi 3 Ekim 2026
Sunucu Kodu Olmadan Adil Liderlik Tabloları: Doğrulanmış Turlar, Sus Paketleri ve Oyuncu Profilleri Yapay zekâ yardımıyla oluşturuldu

Özet olarak

Kurun: sunucu kodu yazmadan sunucu doğrulamalı liderlik tabloları, tek kullanımlık tur biletleri, sus paketleri, sunucu parası ve oyuncu profilleri.

Yeni bir liderlik tablosunun ilk haftası genellikle aynı şekilde biter: dürüst ilk on ile tablonun geri kalanı arasında bir yerde 2.147.483.647 puanlı bir oyuncu durur; bu, işaretli 32-bit bir tamsayının alabileceği en büyük değerdir. O turu kimse oynamadı. Onu bir bellek düzenleyici ya da araya giren bir Proxy yazdı, çünkü çoğu liderlik tablosunda oyun istemcisi hem tek tanık hem de hakemdir.

Bu sürüm bu zayıf noktayı iki taraftan ele alıyor. Validated Actions, neyin sayılacağına dair kararı sunucuya taşıyor; yeni sus tur paketleri ise her şüpheli turu eksiksiz, yeniden oynatılabilir bir delil dosyası olarak saklıyor. Aynı zamanda liderlik tabloları daha kişisel hale geliyor: her kayıt artık avatar, çerçeve ve en fazla üç rozet içeren bir oyuncu profili taşıyor ve kozmetikler hediye kodlarıyla açılabiliyor. Aşağıda her parçanın nasıl çalıştığını, arkasındaki sayıları, Unity ve Godot için çalışan kodu ve buna güvenmeden önce bilmenizi istediğimiz sınırları bulacaksınız.

İstemcinin Bildirdiği Skorlara Neden Güvenilemez

Klasik bir skor gönderimi tek bir istektir: oyuncu ID'si, skor, bitti. Sunucunun o tur hakkında bildiği her şey, bu konuda yalan söylemekte en çok çıkarı olan cihazdan gelir. Alışılmış karşı önlemlerin hepsi aynı kusuru paylaşır:

  • İsteği istemcide imzalamak. İmzalama anahtarı build'inizin içinde dağıtılır. Onu bir IL2CPP binary'sinden ya da bir GDScript export'undan çıkarmak bir öğleden sonranızı alır ve o andan itibaren sahte istekler tamamen geçerli görünür.
  • Skoru bellekte gizlemek. Bu, sıradan bellek düzenleyicileri yavaşlatır, ancak HTTP gövdesini yeniden yazan bir Proxy bellek düzeninize hiç dokunmaz.
  • İstemcide makullük kontrolleri. Cihazda çalışan her şey, yine cihazda yamalanarak devre dışı bırakılabilir.

Sağlam cevap şudur: neyin sayılacağına sunucu karar verir. Bunun ders kitabı versiyonu yetkili (authoritative) bir simülasyondur: oyun mantığınız sizin kontrol ettiğiniz donanımda çalışır ve istemci yalnızca girdileri gönderir. Binlerce eşzamanlı turu olan hızlı bir aksiyon oyunu için bu, haftalarca Netcode çalışması artı kalıcı bir barındırma faturası demektir. Çoğu bağımsız ekibin tam simülasyona ihtiyacı yoktur. Daha ucuz üç garantiye ihtiyaçları vardır: sunucu bir turun ne zaman başladığını, ona hangi kuralların uygulandığını ve sonrasında hangi kanıtların bulunduğunu bilir. Validated Actions tam olarak bu boşluğu doldurur.

Doğrulanmış Bir Tur Nasıl İşler

Doğrulanmış bir tur dört adımdan oluşur ve önemli olan ikisi sunucuya aittir:

  1. Tur başlangıcı. Oyun StartRun çağırır. Sunucu; oyuncuya, API anahtarına ve isteğe bağlı olarak tek bir liderlik tablosuna bağlı, imzalı, tek kullanımlık bir bilet düzenler. Bilet, sunucunun seçtiği bir Seed (0 ile 2.147.483.646 arası) ve bir son kullanma süresi taşır: varsayılan olarak 2 saat, 60 saniye ile 6 saat arasında yapılandırılabilir.
  2. Oyun. Oyun, rastgelelik üretecini sunucunun Seed'i ile başlatır ve oyuncunun girdilerini kompakt bir bayt günlüğüne kaydeder.
  3. Tur sonu. Oyun, SubmitValidated çağrısını skor, isteğe bağlı bir aşama, isteğe bağlı kazanılan değerler ve girdi günlüğüyle yapar. SDK, günlüğün SHA-256 hash'ini gönderir; böylece günlüğün kendisi şimdilik cihazda kalır.
  4. Sunucu kontrolü. Sunucu bileti doğrular, süreyi kendisi ölçer (biletin düzenlenmesinden gönderime kadar) ve kurallarınızı uygular. Ancak bundan sonra herhangi bir şey yazar.

Bir bilet, tüm bölgelerde tam olarak bir kez sayılır. Reddedilen bir tur da biletini yakar; böylece kimse aynı bileti biraz daha düşük skorlarla yeniden deneyerek eşiklerinizi yoklayamaz. Seed'i sunucu seçtiği ve oyuncu başına saatlik bilet sayısını sınırladığı (varsayılan 60) için şanslı bir Seed peşinde koşmak hem yavaşlar hem de göze batar.

var va = ValidatedActionsManager.Instance;

// Run start: single-use ticket plus server seed
ValidatedRun run = await va.StartRun("weekly");
if (run == null) { Debug.Log(va.LastErrorCode); return; } // e.g. RUN_RATE_LIMITED
var random = new System.Random(run.seed);

// ... play, record the inputs into inputLog (byte[]) ...

// Run end: the SDK hashes the log and sends the SHA-256 with the ticket
ValidatedSubmitResult result = await va.SubmitValidated(18250, inputLog);
if (result == null)
{
    // e.g. DURATION_TOO_SHORT; HasActiveRun tells you if the ticket survived
    Debug.Log($"{va.LastErrorCode}, run kept: {va.HasActiveRun}");
    return;
}
Debug.Log($"Rank {result.rank}, {result.durationSeconds}s measured by the server, sus: {result.sus}");

Aynı akış Godot'ta (Horizon.validatedActions.startRun ve submitValidated) ve Unreal'da (Horizon->ValidatedActions) da mevcut; her biri için SDK'da eksiksiz bir örnek bulunuyor.

Yalnızca Sunucunun Bildiği Kurallar

Her API anahtarı, Dashboard'da form ya da JSON olarak düzenlenen tek bir kural setine sahiptir. Kurallar hiçbir zaman uygulama yanıtlarında veya hata mesajlarında görünmez: istemci yalnızca makine tarafından okunabilir bir kod öğrenir. İşte dalga tabanlı bir arcade oyunu için gerçekçi bir kural seti:

{
  "formatVersion": 1,
  "ticketLifetimeSeconds": 3600,
  "maxRunsPerPlayerPerHour": 30,
  "defaults": {
    "maxScore": 250000,
    "minDurationSeconds": 45,
    "maxScorePerSecond": 900.0,
    "stages": {
      "wave_10": { "maxScore": 40000, "minDurationSeconds": 60 }
    },
    "soft": { "maxScore": 180000, "maxScorePerSecond": 600.0 }
  },
  "leaderboards": {
    "speedrun": { "minScore": 95000, "minDurationSeconds": 95 }
  },
  "values": {
    "gold": { "maxPerRun": 500, "minPerRun": -1000, "dailyCap": 5000 }
  }
}

weekly tablosunda birkaç gönderimi bu kurallardan geçirelim:

Gönderilen tur Sunucu yanıtı
9.999.999 puan 422 SCORE_ABOVE_MAX
21.000 puan, biletten 3 saniye sonra 422 DURATION_TOO_SHORT
120 saniyede 150.000 puan (saniyede 1.250) 422 SCORE_RATE_TOO_HIGH
240 saniyede 190.000 puan kabul edildi, ancak sus (180.000 olan yumuşak maksimumun üzerinde)
aynı bilet ikinci kez 422 TICKET_CONSUMED

Sunucu sabit bir sırayla kontrol eder (aşama, skor, aşama kuralı, süre, saniye başına skor, kazanılan değerler, ardından yumuşak eşikler) ve ilk başarısızlık belirleyici olur. Skor kuralları yalnızca bir tabloya bağlı turlara uygulanır; tablosu olmayan bir turun da aşama ve süre kuralları yine de kontrol edilir.

Kurallar oturduğunda tabloyu "Yalnızca doğrulanmış gönderimler" moduna geçirin. O andan itibaren bu tabloya yapılan düz bir SubmitScore, 403 VALIDATED_SUBMIT_REQUIRED ile reddedilir; diğer tüm tablolar normal gönderimleri kabul etmeye devam eder. Kural seti boş olsa bile, yalnızca doğrulanmış gönderim kabul eden bir tablo; sunucu biletlerini, tek kullanımı, oyuncuya bağlanmayı, saatlik limitleri ve saklanan bir günlük hash'ini garanti eder.

Yumuşak Eşikler ve Sus Turlar

Katı kurallar reddeder. Yumuşak eşikler ise bir turu yalnızca işaretler ve başlamanız gereken yer de burasıdır: bir haftalık yumuşak limitler, sayıları katı retlere dönüştürmeden önce gerçek oyunun neye benzediğini size gösterir. Bir tur, kabul edilmiş ve en az bir yumuşak eşiği aşmışsa sus sayılır. Reddedilen turlar ve teknik hatalar asla sus değildir. Gönderim sonucu basit bir sus bayrağı taşır; hangi eşiğin tetiklendiği sunucuda kalır.

Asıl ilginç kısım bundan sonra olanlardır. Bu güncellemeyle sunucu, oyuncunun en iyi skorundan, Top N'den veya sonraki skor değişikliklerinden bağımsız olarak her sus tur için bir sus paketi saklıyor. Önce absürt bir tur, ardından normal bir tur gönderen bir hileci artık kanıtı daha iyi bir kaydın altına gömemez.

Başlangıç bağlamı: turun neyle başladığı

Bir Replay ancak başlangıç koşullarını birebir yeniden üretebiliyorsanız işe yarar. Bu yüzden sunucu artık StartRun anında başlangıç bağlamını kaydediyor: geçerli kural versiyonu, oyuncunun Cloud Save'inin gerçek baytları (SHA-256 ve revizyon ile), sunucuya ait değerler, Seed ve başlangıç zamanı. Oyun da hikâyenin kendi tarafını ekleyebilir:

var context = new ValidatedRunContext(
    gameVersion: Application.version,          // e.g. "1.4.2"
    contentVersion: "levels-2026-10",
    simulationVersion: "sim-7",
    replayFormatVersion: "input-v3",
    contentDigest: ValidatedActionsManager.ComputeInputLogHash(levelBytes),
    initialState: serializedLevelState);       // bytes the simulation starts from

ValidatedRun run = await ValidatedActionsManager.Instance.StartRun("weekly", context);

Sunucu değerleri ile istemci değerleri kesinlikle ayrı tutulur ve her ikisi de SHA-256'sı bilet üzerinde saklanan kanonik bir metni besler. Gönderim ayrıca başlangıçtaki kural versiyonuna göre kontrol edilir; yani bir tur devam ederken bir limiti sıkılaştırmak o turun kararını asla değiştirmez. Kural düzenlemelerinin yoklama aracı olarak kötüye kullanılmasını önlemek için değişiklikler API anahtarı başına saatte 30 ile sınırlıdır.

Bir sus paketi neler içerir

Her paket tur ID'si altında saklanır ve bir inceleyicinin ihtiyaç duyduğu her şeyi bir araya getirir: başlangıç bağlamı, bağlı kurallar, sonuç, turdan önceki ve sonraki sunucuya ait değerler ve girdi günlüğü (SDK, tıpkı Top N turlarında olduğu gibi bunu otomatik olarak yükler). Dashboard'un Review sekmesinde sus'a göre filtreleyip bir paketi ZIP dosyası olarak dışa aktarırsınız:

<runId>.hzn-va-package.zip
├── manifest.json        format horizon.validated-actions.package v1
├── start-context.txt    canonical start context, hashed on the ticket
├── rules.json           the rule version the run was checked against
├── cloud-save.bin       the player's save at run start
├── initial-state.bin    the game's initial simulation state
├── input-log.bin        the recorded inputs
└── SHA256SUMS

Sunucu her dışa aktarımda tüm sağlama toplamlarını yeniden hesaplar ve sonucu manifest.integrity alanında ve X-Package-Integrity header'ında (ok veya mismatch) bildirir. Tek bir parça boyut sınırına sığmazsa yalnızca sağlama toplamı tutulur ve parça OMITTED_SIZE_LIMIT olarak işaretlenir; böylece neyin eksik olduğunu ve nedenini her zaman bilirsiniz.

cloud-save.bin, initial-state.bin, Seed ve input-log.bin dosyalarını kendi deterministik simülasyonunuza verin; ya skoru yeniden üretirsiniz ya da üretemezsiniz. Replay'ler, kararlar ve yaptırımlar sizde kalır; sunucu işaretler ve arşivler, oyun kodunuzu asla çalıştırmaz.

İstemcinin Yazamadığı Para Birimi

Hile yapmaya değer tek şey liderlik tabloları değildir. Sunucuya ait değerler ile aynı kural setinde gold veya gems gibi sayaçlar tanımlarsınız. Bunları yalnızca kabul edilmiş doğrulanmış turlar değiştirir: bakiye ayarlayan ne bir uygulama endpoint'i ne de bir SDK metodu vardır. Yukarıdaki JSON'daki kural şöyle okunur:

  • maxPerRun: 500: 800 altın talep eden bir tur EARNED_ABOVE_MAX ile reddedilir.
  • minPerRun: -1000: bir tur en fazla 1.000 altın harcayabilir; bakiyeden fazlasını harcamak INSUFFICIENT_BALANCE ile başarısız olur.
  • dailyCap: 5000: UTC günü başına pozitif krediler reddedilmez, kırpılır. Bugün zaten 4.800 kazanmış ve 500 altınlık bir turu bitiren bir oyuncunun hesabına 200 yatırılır.

Yanıt, dokunulan her anahtar için requested ve credited değerlerini gösterir; böylece oyun, coinleri sessizce kaybetmek yerine "günlük limite ulaşıldı" mesajı gösterebilir. Satın almalar için kural basittir: öğeyi yalnızca harcama tamamen işlendiğinde verin (Unity ve Unreal'da IsFullyCredited). Bu, yükseltme mağazalarını istemci taraflı varsayılanlardan uzaklaştırmanın ardındaki mantığın aynısıdır: cihaz talep edebilir, yalnızca sunucu verir.

Cloud Save eskisi gibi çalışmaya devam eder ve bir aynaya dönüşür. Çevrimdışı gösterim için kabul edilen her turdan sonra sunucu değerlerini kayda kopyalayın, açılışta bu kopyanın üzerine GetState ile yazın ve kayıttaki bir değeri asla bakiye olarak geri göndermeyin.

Her Liderlik Tablosu Kaydında Oyuncu Profilleri

Bu güncellemenin ikinci yarısı sayılardan çok tablodaki insanlarla ilgili. Top, Around ve Rank'taki her liderlik tablosu kaydı artık avatarId, frameId ve en fazla üç badges içeren bir profil taşıyor:

{ "position": 1, "username": "Gravedigger", "score": 15000,
  "profile": { "avatarId": "avatar.zombie_07", "frameId": "frame.gold", "badges": ["badge.supporter"] } }

Sunucu profili, görünen adla birlikte kullanıcı adı önbelleğinden okur; böylece bir top listesi kayıt başına ek sorgu maliyeti getirmez. Profil değişiklikleri en geç 10 dakika içinde tüm sunucularda görünür.

Görseller değil, ID kataloğu

Her proje Dashboard'da bir kozmetik kataloğu tutar. Bir kaydın bir ID'si (örneğin avatar.zombie_07), bir türü (avatar, frame veya badge) ve bir locked bayrağı vardır. Ücretsiz kayıtları her oyuncu seçebilir; kilitli kayıtları ise yalnızca onları açmış oyuncular. Sunucu yalnızca ID'leri saklar, asla görsel saklamaz: oyununuz her ID'yi kendi sprite'larına eşler, böylece görseller build'inizde kalır ve yeni bir çerçeve yalnızca tek bir katalog satırına mal olur.

Godot'ta bir tablo satırını render etmek şöyle görünür:

var entries := await Horizon.leaderboard.getTop(10, true, "weekly")
for entry in entries:
	var row := RowScene.instantiate()
	row.set_rank(entry.position, entry.username, entry.score)
	if entry.profile.hasAvatar():
		row.set_avatar(AVATARS.get(entry.profile.avatarId, DEFAULT_AVATAR))
	if entry.profile.hasFrame():
		row.set_frame(FRAMES.get(entry.profile.frameId))
	for badge_id in entry.profile.badges:
		row.add_badge(BADGES.get(badge_id))
	$Board.add_child(row)

Bilinmeyen ID'ler (örneğin bir katalog kaydını sildikten sonra) basitçe "ayarlanmamış" olarak render edilmelidir: oyuncular profillerini değiştirene kadar eski ID'yi korur ve hiçbir şey bozulmaz.

Hediye kodlarıyla kilit açma

Kilit açmalar yalnızca sunucu tarafından yazılır; bugün grants içeren hediye kodlarıyla ya da Dashboard'da elle. Bir oyuncu en fazla 25 kilit açmaya sahip olabilir. Grants içeren bir kodu kullanmak, verilen ID'leri döndürür ve önbellekteki profili düşürür; böylece bir sonraki GetProfile yeni öğeyi kullanılabilir olarak gösterir:

var result := await Horizon.giftCodes.redeem("SPOOKY-FRAME-2026")
if not result.is_empty() and not result["grantedUnlocks"].is_empty():
	await Horizon.playerProfile.getProfile() # reloads catalog and unlocks
	if Horizon.playerProfile.isAvailable("frame.spooky"):
		await Horizon.playerProfile.setProfile("avatar.zombie_07", "frame.spooky", ["badge.supporter"])

Kilidi açılmamış bir öğeyi seçmek 403 COSMETIC_LOCKED ile, üçten fazla rozet seçmek ise 400 INVALID_BADGES ile başarısız olur. Bu sayede etkinlik ödülleri, Discord çekilişleri ve destekçi ayrıcalıkları, özel bir kilit açma endpoint'i gerekmeden her biri tek bir hediye koduna dönüşür. Eski API'den kalan bir kalıntı da tamamen kaldırıldı: skor gönderimlerindeki metadata parametresi hiçbir zaman saklanmıyordu ve artık kullanımdan kaldırıldı. Oyuncuya özel bilgilerin yeri profildir.

Skor Limitleri Artık API Anahtarı Başına Sayılıyor

Tek hesapta birden fazla oyun veya ortam çalıştıran stüdyolar için gerçek etkisi olan daha küçük bir değişiklik: skor limiti artık API anahtarı başına uygulanıyor. Her anahtar, kademenin izin verdiği kadar skor satırı tutabilir (bir satır, bir tablodaki bir oyuncudur), bu sayı o anahtarın tüm tabloları üzerinden toplanır ve her ek anahtar kendi tam kotasını alır. Mevcut satırlar her zaman iyileştirilebilir; anahtar dolduğunda yalnızca bir oyuncunun bir tablodaki ilk skoru 403 alabilir.

Adil Liderlik Tabloları İçin En İyi Uygulamalar

  1. Yumuşak başlayın, sonra sertleştirin. Bir hafta boyunca yalnızca soft eşiklerle çalışın, Review sekmesindeki sus turlara bakın ve sayıları en iyi meşru turun yüzde 20 ila 30 üzerinde bir güvenlik payıyla katı kurallara dönüştürün.
  2. StartRun'ı tur gerçekten başladığında çağırın. Saat biletle birlikte başlar. Turu menüde ya da 40 saniyelik bir yükleme ekranından önce başlatmak minDurationSeconds değerini anlamsız kılar.
  3. Girdi günlüklerini küçük ve deterministik tutun. Sabit girdi karelerini delta kodlamayla kaydedin. Kanıt, günlük başına 32 KB ile sınırlıdır ve saniyede 30 girdi karesi ile kare başına 1 bayt kullanan 10 dakikalık bir tur buna rahatça sığar.
  4. Başlangıç bağlamındaki her şeyi versiyonlayın. simulationVersion ve contentDigest olmadan geçen aydan kalma bir paket bu ayın dengelemesine karşı yeniden oynatılabilir ve yanlış bir nedenle başarısız olabilir.
  5. Kaydı asla kaynak olarak değil, ayna olarak görün. Bakiyeler sunucudan Cloud Save'e akar, asla tersi olmaz.

Neleri Yapmaz

Bunu açıkça söylemeyi tercih ediyoruz. Kurallarınız dahilinde makul değerler bildiren değiştirilmiş bir istemci yine de kabul edilir: Validated Actions bir hilecinin ne kadar kazanç sağlayabileceğini sınırlar ve şüpheli turları incelenebilir kılar, ama hileyi imkânsız hale getirmez. Sunucu sınırları, zamanlamayı ve tek kullanımı kontrol eder ve kanıtları arşivler, ancak oyun kodunuzu çalıştırmaz veya yeniden oynatmaz ve kanıtlar üzerinde henüz otomatik bir karar mekanizması yoktur. Validated Actions yalnızca bulutta çalışır; self-hosted simpleServer üzerinde SDK'lar NOT_SUPPORTED bildirir. Her tur oturum açmış bir oyuncu gerektirir; bir liderlik tablosundaki turlar ayrıca bir görünen ad gerektirir.

Kademeye Göre Kapasite

Bu yazıdaki her özellik FREE dahil her planda kullanılabilir. Yalnızca kapasite artar:

FREE BASIC PRO ENTERPRISE
Hesap ve UTC saati başına doğrulanmış tur 300 3.000 20.000 200.000
API anahtarı başına sunucuya ait değer 8 16 32 64
Top N turları için kanıt slotu 50 500 2.500 25.000
Saklanan sus paketi 10 100 1.000 10.000
Sus paketi saklama süresi 14 gün 30 gün 90 gün 180 gün
API anahtarı başına kozmetik kataloğu 50 200 500 1.000
API anahtarı başına skor satırı 5.000 25.000 200.000 2.500.000

Sus paketi kotası dolduğunda yeni sus turlar yine kabul edilir ve sus olarak bildirilir, yalnızca arşivlenmezler. Saklanan paketler asla yenileri tarafından dışarı itilmez.

Başlarken

Unity, Godot veya Unreal için en son SDK'ya güncelleyin, bir tablo seçin ve yalnızca yumuşak eşikler içeren bir kural seti ekleyin. StartRun çağrınıza bir ValidatedRunContext ekleyin, ardından kozmetik kataloğunuzu oyununuzda zaten bulunan avatar ve çerçevelerle doldurun. Validated Actions özellik sayfası ve liderlik tablosu özellik sayfası kuralları ve limitleri özetler, hızlı başlangıç kılavuzu ise üç motorun tamamını adım adım anlatır. Bir sonraki liderlik tablonuzu hem adil hem de kişisel yapmaya hazır mısınız? horizOn'u ücretsiz deneyin ya da API dokümantasyonuna dalın.