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/1mi?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:
HungrySickTiredExhaustedDepressedFreezingWounded
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,
callRemoteile ç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. 🛠️
Gringos