This War of Mine Modunu Nasıl Geliştirdim? – Native x64, AOB ve RIP-Relative Yapısı

Araç & yöntem #this-war-of-mine#native-x64#aob-signature#trainer-gelistirme#orga-hub 0 cevap 13 okunma

This War of Mine için hazırladığım mod, ORGA Hub’daki diğer birçok içerikten teknik olarak biraz farklı bir proje oldu.

Burada Unity Mono, Unreal Engine Reflection veya oyunun hazır bir scripting runtime’ı üzerinden ilerlemedim.

Doğrudan çalışan 64-bit oyun sürecinin native kodu ve runtime yapılarıyla çalışmam gerekti.

Bu nedenle This War of Mine için ORGA Hub içerisinde oyuna özel bir twomNative katmanı geliştirdim.

Bu konuda moddaki 19 özelliği tekrar anlatmak yerine bu native katmanın nasıl çalıştığını ve geliştirme sırasında karşılaştığım önemli problemleri anlatacağım.

🧠 İlk Problem: Sabit Adreslere Güvenemezdim

Native trainer geliştirirken en kolay görünen yöntemlerden biri şudur:

Bir adres bulursunuz.

Örneğin:

This War of Mine.exe + XXXXXXX

Trainer her açıldığında aynı RVA üzerinden bu adrese gider.

Bazı oyunlarda bu yeterince stabil olabilir.

Ama This War of Mine üzerinde çalışırken yalnızca sabit RVA'lara güvenmenin benim istediğim seviyede sağlam olmadığını gördüm.

Aynı executable içerisinde aradığım yapının:

  • Nereden çağrıldığını

  • Hangi global nesneye ulaştığını

  • Hangi field'ı kullandığını

  • Gerçekten beklediğim fonksiyon olup olmadığını

da doğrulamam gerekiyordu.

Bu yüzden ana çözümüm:

AOB Signature + RIP-relative çözümleme + runtime doğrulaması

oldu.

🔎 Önce Benzersiz Kod İmzasını Buluyorum

Örneğin oyunun:

  • HealAll

  • Bulletproof

  • Zaman sistemi

  • Envanter

  • Craft

  • Mermi tüketimi

  • DayDuration

gibi sistemleri için çalıştırılabilir kod içerisinde ayırt edilebilir instruction dizileri bulunuyor.

ORGA Hub önce ilgili AOB imzasını tarıyor.

Ama burada önemli bir nokta var:

İmzanın bulunması tek başına:

Tamam, doğru adres bu.

demek için yeterli değil.

Bir imzanın yanlış veya benzer başka bir fonksiyona denk gelmesi ihtimalini mümkün olduğunca azaltmam gerekiyor.

🧩 RIP-Relative Adresleme

64-bit Windows oyunlarında kodun global verilere erişirken sık kullandığı yöntemlerden biri RIP-relative adresleme.

Basit bir örnek düşünelim:

Kod:

Şu global nesneyi kullan.

diyor ama içerisinde doğrudan:

0x00007FF....

gibi nihai adres bulunmuyor.

Bunun yerine mevcut instruction konumuna göre bir displacement bulunuyor.

ORGA Hub burada instruction'ı okuyup:

Instruction adresi

Instruction uzunluğu

Relative displacement

üzerinden gerçek hedef adresi hesaplıyor.

Bu sayede ASLR nedeniyle modülün bellekte hangi taban adrese yüklendiğine bağımlı kalmıyorum.

🛡️ Adresi Bulmak Yetmiyor, Doğrulamam Gerekiyor

Bu projede özellikle önem verdiğim noktalardan biri bu.

Trainerın:

Bir adres buldum, yazıyorum.

şeklinde davranmasını istemiyorum.

Örneğin dünya nesnesini çözdüğümde yalnızca pointer'ın sıfırdan farklı olup olmadığına bakmıyorum.

Ardından:

  • Entity sayısı mantıklı mı?

  • Entity listesi gerçekten var mı?

  • Pointer beklenen belleği gösteriyor mu?

gibi kontroller uygulanıyor.

Benzer şekilde farklı alanlarda da:

  • Boolean değer gerçekten 0/1 mi?

  • Float mantıklı bir aralıkta mı?

  • VTable gerçekten oyun modülünün içerisinde mi?

  • Çözülen fonksiyon executable modül aralığında mı?

  • Field offset mantıklı bir aralıkta mı?

kontrol ediliyor.

Bu nedenle twomNative katmanının önemli bir bölümü aslında hileyi uygulamaktan çok hedefin doğru olduğunu kanıtlamaya ayrılmış durumda.

🌍 Aktif Oyun Dünyasını Bulmak

Karakterlerle ilgili işlemlerde önce oyunun aktif world yapısına ulaşmam gerekiyor.

Bunu doğrudan sabit bir world pointer yazmak yerine oyun kodundan çözümlediğim bağlantı üzerinden yapıyorum.

World bulunduğunda içerisindeki aktif entity listesi de kontrol ediliyor.

Böylece örneğin:

Tüm Karakterleri İyileştir

işleminde oyunun gerçek aktif dünyası üzerinden ilerleyebiliyorum.

Burada ayrıca güzel bir avantaj var:

Oyunun kendi HealAll fonksiyonunu bulduğum için karakterlerin sağlık değerlerini tek tek tahmin edip değiştirmek zorunda değilim.

Tek tuşta doğrudan oyunun kendi yerel fonksiyonunu çalıştırabiliyorum.

❤️ İhtiyaç Sistemini Ayrı Çözmek Gerekti

Açlık, hastalık, depresyon, yorgunluk ve benzeri karakter durumlarında ise başka bir yapı kullanılıyor.

Aktif karakterleri bulup ilgili need değerlerini oyun tarafından kullanılan isimleri üzerinden işleyebiliyorum.

Örneğin:

  • Hungry

  • Sick

  • Tired

  • Exhausted

  • Depressed

  • Freezing

  • Wounded

gibi gerçek durumlar takip ediliyor.

Bu sayede örneğin Yorgunluk Yok özelliğinde yalnız tek bir varsayımsal fatigue değerini sıfırlamak yerine oyunun kullandığı hem:

Tired

hem de:

Exhausted

durumlarını temizleyebiliyorum.

🎒 Envanter Sistemi Projenin En İlginç Kısımlarından Biri Oldu

This War of Mine envanter sistemi üzerinde çalışırken istediğim şey sadece bellekte bir miktar bulup 99 yazmak değildi.

Oyunun gerçek global barınak envanteri üzerinde çalışmak istedim.

Bunun için oyunun kendi:

GetGlobalItemCount

ve

AddItems

fonksiyonlarını çözümledim.

Ayrıca bu fonksiyonların beklediği oyun içi string yapısını oluşturabilmek için ilgili string constructor ve destructor fonksiyonlarını da bulmam gerekti.

Yani bir eşya eklerken ORGA Hub kabaca:

Eşya ID'sini hazırla

Oyunun string objesini oluştur

Mevcut miktarı oyunun kendi fonksiyonuyla sorgula

Gerekli farkı hesapla

Oyunun AddItems fonksiyonunu çağır

Yeni miktarı tekrar sorgula

Sonucun gerçekten uygulandığını doğrula

şeklinde ilerliyor.

🧵 Sonra Bir Sorun Çıktı: Her Fonksiyon Remote Thread'den Çağrılamıyor

İlk bakışta:

Fonksiyon adresini buldum, callRemote ile çağırırım.

demek kolay.

Ama oyun motorlarında bazı işlemler thread'e bağlı olabiliyor.

Envanter sistemi üzerinde çalışırken bazı native işlemleri dışarıdan oluşturulan rastgele bir thread üzerinden çağırmanın güvenilir olmadığını gördüm.

Burada ihtiyacım olan şey:

Oyunun gerçek ana thread'inde fonksiyonu çalıştırmak.

🪟 WindowProc Üzerinden Main Thread Köprüsü

Bunun için oyunun Windows pencere sistemini kullanan ayrı bir köprü geliştirdim.

Önce:

This War Of Mine

ana penceresi bulunuyor.

Sonra pencerenin gerçek WindowProc adresi okunuyor.

Ama yine sadece adresi kabul etmiyorum.

Bulunan WindowProc'un önceden çözümlediğim This War of Mine koduyla gerçekten eşleştiğini doğruluyorum.

Ardından ORGA Hub'ın genel main-thread dispatcher katmanı buraya bağlanıyor.

Çağrı yapılması gerektiğinde Windows mesaj sistemi üzerinden oyun thread'i tetikleniyor ve fonksiyon oyun tarafından kullanılan doğru thread üzerinde çalıştırılıyor.

Bu sistem özellikle:

  • Barınağa eşya ekleme

  • Belirli eşya miktarını tamamlama

işlemlerinde önemli hale geldi.

📦 Neden “99 Yaz” Yerine “99’a Tamamla” Dedim?

Barınağa tüm eşyaları eklerken de doğrudan envanter belleğini yeniden oluşturmuyorum.

Önce gerçek mevcut miktarı soruyorum.

Örneğin:

Materials = 34

ve hedef:

99

ise eklenecek miktar:

65

olarak hesaplanıyor.

Sonra oyunun kendi AddItems fonksiyonuna yalnızca:

65 ekle

deniliyor.

Ardından miktar yeniden okunuyor.

Bu nedenle butona tekrar basıldığında:

99 → 198 → 297

gibi anlamsız şekilde büyümüyor.

Zaten 99 varsa hiçbir şey eklenmiyor.

📚 49 Eşya Nereden Geldi?

Burada herhangi bir internet listesini veya tahmini eşya adlarını kullanmadım.

Oyunun kendi kaynaklarında bulunan gerçek CollectableItems tablosunu temel aldım.

Buradan 49 kanonik eşya kimliği çıkardım.

Bunların arasında örneğin:

  • Bandages

  • CannedFood

  • Coffee

  • Meds

  • RawFood

  • Vegetables

  • Crowbar

  • Hatchet

  • LockPick

  • Shovel

  • Materials

  • Parts

  • Water

  • Wood

  • Ammo

  • Pistol

  • Shotgun

  • Helmet

  • Vest

gibi gerçek oyun kimlikleri bulunuyor.

ORGA Hub’daki Eşya Defteri de bu listenin kullanıcıya okunabilir hale getirilmiş arayüzü.

Eşya Defteri'nin kendisini ayrı rehber konusunda anlatacağım.

♾️ Sınırsız Kaynak İçin Envanteri Freeze Etmedim

Bu özellikte özellikle farklı bir çözüm kullandım.

İlk akla gelen yöntem:

Bütün item miktarlarını sürekli sabitle.

olabilir.

Ama bunu yaptığımda oyunun normal kullanım akışına gereksiz yere müdahale etmiş olurum.

Örneğin karakter yemek yiyor.

Ben istiyorum ki:

  • Yemek yeme animasyonu çalışsın

  • Karakterin açlığı azalsın

  • Oyunun bildirimi gelsin

  • İlgili item kullanılabilsin

  • Sadece stoktan düşme gerçekleşmesin

Bu yüzden bütün envanteri freeze etmek yerine yalnızca global envanterden miktar düşüren adımı hedefledim.

Sonuç:

Kullanım işlemi çalışıyor

ama

envanter azaltma aşaması atlanıyor.

Bu daha temiz bir davranış sağladı.

🔧 Üretim Sistemi de Ayrı Ayrı Çözüldü

Üretim tarafında iki farklı özelliğin birbirini bozmaması gerekiyordu:

Anında Üretim

ve

Üretim Sonucu Çarpanı

Bunları aynı noktadan yapmak yerine oyunun üretim zincirinin farklı aşamalarını hedefledim.

⚡ Anında Üretim

Burada amaç:

Craft süresini sıfıra yaz.

değildi.

Oyunun crafting progress sorgusunun:

tamamlandı

sonucunu döndürmesini sağlıyorum.

Bundan sonrasını oyunun kendi Lua/crafting akışı gerçekleştiriyor.

Yani sonuç yine oyunun normal Complete yolu üzerinden teslim ediliyor.

Bu yaklaşım üretim sonucunu elle envantere eklemekten daha düzgün.

✖️ Üretim Sonucu Çarpanı

Çarpan tarafında ise yalnızca gerçekten craft sonucu olarak envantere teslim edilen miktarı hedeflemek istedim.

Genel AddItems fonksiyonunu çarpsaydım:

  • Loot

  • Debug item ekleme

  • Başka inventory işlemleri

de istemeden çarpılabilirdi.

Bu yüzden crafted-output transaction'ının kendisine özel bir hook kullandım.

Böylece:

2x üretim

yalnızca gerçekten üretilen item miktarını etkiliyor.

Genel eşya ekleme sistemine dokunmuyor.

⏱️ Zamanı Dondurmak Beklediğimden Daha Karmaşıktı

This War of Mine'da tek bir global:

GameTime = X

değeri dondurmak yeterli değildi.

Barınak ve yağma tarafının zaman davranışları farklı.

Üstelik benim istediğim:

oyunun tamamını pause etmek

değil.

Sadece saat ilerlemesini durdurmak.

Karakterlerin:

  • Hareket etmesi

  • AI davranışları

  • Üretim

  • Diğer simülasyonlar

çalışmaya devam etmeli.

🏠 Barınak ve 🌙 Yağma İçin Ayrı Çözüm

Barınak tarafında gün ilerleme sistemini kontrol eden frame update akışı hedefleniyor.

Yağma sırasında ise ayrı bir elapsed timer bulunuyor.

Burada özellikle yalnız gece saatinin ilgili elapsed yazma işlemini durduruyorum.

Sibling sayaçları çalışmaya devam ediyor.

Bunun sonucu:

Saat duruyor

ama

oyun dünyası yaşamaya devam ediyor.

Zaman Dondur özelliğinin tüm oyunu kilitlememesinin nedeni bu.

🔫 Sınırsız Mermi

Sınırsız mermi için de cephane miktarını sürekli yüksek değerde tutmak yerine gerçek ammo consumption yolunu buldum.

Burada mühimmat azaltma çağrısı hedefleniyor.

Özellik açıldığında tüketim çağrısı geçici olarak atlanıyor.

Kapatıldığında ise saklanan özgün çağrı geri getiriliyor.

Bu mantığı native patch kullandığım diğer özelliklerde de korumaya çalıştım:

Açarken değiştir

Özgün kodu kaydet

Çalışırken değişikliği doğrula

Kapatınca birebir geri yükle

🛠️ Dayanıklılık Sisteminde İlk Yaklaşımı Bilerek Terk Ettim

Trainer geliştirmede bazen ilk bulduğunuz adres çalışıyor gibi görünür ama aslında doğru sistemi temsil etmiyordur.

Dayanıklılık bunun iyi örneklerinden biri oldu.

İlk araştırmalarda item use count tarafında bir nokta bulunmuştu.

Ancak bunun gerçek tool durability mekanizması olmadığı anlaşıldı.

Kod içerisinde bunun eski ORGA sürümlerinden kalmış patch'ini sadece güvenli şekilde geri yükleyebilmek için compatibility desteğini korudum.

Yeni sistem ise gerçek:

DamageOnUsage

akışını takip ediyor.

Oyunda alet dayanıklılığını düşüren iki gameplay yolu tespit edildi ve gerçek durability/fracture fonksiyonuna yapılan çağrılar hedeflendi.

Yani:

Bir şey buldum ve çalışıyor gibi, yayınlayalım.

yerine doğru mekanizmayı bulana kadar araştırmayı sürdürdüm.

Bence trainer geliştirmede bu ayrım önemli.

🧯 Crash Sonrası Recovery'yi de Düşündüm

Native patch kullanan bir trainerda kullanıcı:

  • Oyunu kapatabilir

  • Oyun crash olabilir

  • Hub kapanabilir

  • Özellik açıkken bağlantı kopabilir

Bu yüzden sadece:

enable

ve

disable

yazmak yeterli değil.

twomNative katmanında yapılan önemli değişikliklerin özgün durumları recovery kaydında tutuluyor.

Yeni oturum başladığında ORGA Hub gerekirse:

  • Eski patch'in hâlâ orada olup olmadığını kontrol edebiliyor

  • Özgün call bytes'larını doğrulayabiliyor

  • Beklenen patch dışında başka kod varsa müdahaleyi reddedebiliyor

  • Eski ORGA sürümlerinden kalan bazı patch formatlarını güvenli şekilde geri alabiliyor

Bu özellikle native oyunlarda önemli.

🚫 “Ne Bulursan Üzerine Yaz” Mantığı Yok

Kod tarafında birçok yerde özellikle şu tarz kontroller var:

Beklediğim özgün kod mu?

veya:

Benim daha önce uyguladığım patch mi?

Bu ikisinden hiçbiri değilse işlem duruyor.

Trainer:

Burada beş byte var, NOP'layalım.

demiyor.

Çünkü aradaki bir oyun güncellemesi kodu değiştirmiş olabilir.

Yanlış instruction'ı patchlemek trainerın çalışmamasından daha kötü sonuç doğurabilir.

Bu yüzden beklenen yapı doğrulanmıyorsa fail etmek, tahmin ederek devam etmekten daha doğru.

🔍 AOB'nin Tek Başına Yeterli Olmamasının Nedeni de Bu

This War of Mine projesi benim için bunun güzel bir örneği oldu.

Sağlam native trainer mantığım şu hale geldi:

AOB ile kodu bul

RIP-relative bağlantıları çöz

Hedef adres modül aralığında mı kontrol et

Runtime nesnesini doğrula

Field / VTable / değer aralıklarını kontrol et

İşlemi uygula

Uygulamanın sonucunu tekrar doğrula

Yani AOB sadece başlangıç noktası.

Asıl güvenilirlik daha sonraki doğrulama zincirinden geliyor.

🧱 Neden Oyuna Özel twomNative Katmanı Yazdım?

Bu işlemlerin tamamını trainer tanım dosyasının içine ayrı ayrı koymak mümkün değildi.

This War of Mine:

  • Kendi native fonksiyon çağrılarına

  • String yapılarına

  • Main-thread gereksinimlerine

  • Envanter host çözümüne

  • Craft zincirine

  • Zaman sistemine

  • Recovery mantığına

sahip.

Bu nedenle ORGA Hub'ın genel memory API'sinin üzerine:

twomNative

adında oyun özelinde bir abstraction katmanı koydum.

Trainer tanımı artık:

unlimitedResources

diyor.

Bunun arkasındaki onlarca doğrulama, resolver ve patch işlemini native helper yönetiyor.

Bu hem trainer tanımını temiz tutuyor hem de bütün This War of Mine araştırmasını tek yerde toplamamı sağlıyor.

🧪 Araştırma İçin Ayrı Araçlar da Hazırladım

This War of Mine çalışması sırasında yalnızca final trainer dosyasını yazmadım.

Proje içerisinde farklı sistemleri araştırmak ve doğrulamak için ayrı araçlar oluşturdum.

Örneğin:

  • Clock semantic testleri

  • Day progress probe

  • Scavenge clock probe

  • Crafting component trace

  • Crafting field xref

  • Crafting live test

  • Durability smoke test

  • Resource probe

  • Metadata map

  • Regression testleri

gibi yardımcı çalışmalar bulunuyor.

Final trainerda gördüğünüz tek bir seçenek bazen arkasında birkaç ayrı araştırma ve canlı doğrulama çalışmasına dayanıyor.

🎯 Bu Projede Çıkardığım En Önemli Sonuç

This War of Mine modunda benim için asıl mesele 19 tane özellik yapmak değildi.

Önemli olan bu özelliklerin:

  • Gerçek oyun sistemini hedeflemesi

  • Yanlış pointer'a yazmaması

  • Runtime durumunu doğrulaması

  • Gerektiğinde ana thread'i kullanması

  • Oyunun kendi fonksiyonlarından faydalanması

  • Sadece ihtiyaç duyulan noktayı patchlemesi

  • Kapatıldığında özgün davranışı geri getirmesi

  • Oyun sürümü değiştiğinde güvenli şekilde çalışmayı reddedebilmesi

oldu.

Sonuçta ortaya klasik birkaç adres yazan trainer yerine This War of Mine'a özel küçük bir native entegrasyon katmanı çıkmış oldu.

Bence bu proje, ORGA Hub tarafında native x64 oyunlar için bundan sonra kullanacağım yaklaşımın da iyi örneklerinden biri oldu:

Önce hedefi bul, sonra hedefin gerçekten doğru olduğunu kanıtla, en son müdahale et. 🛠️

Cevap yazmak için giriş yapmalısın. Okumak için giriş gerekmez.

Giriş yap