Bahadir Kaptanim,
Vesile oldu, Delphi ile ufak bir örnek yapim derken eski bilgileri güncellemis oldum.
Özellikle siz ve diğer ilgili arkadaşlar için FS'in işleyişi ve add-on yazmak üzerine kısa bir yazı hazırlamaya karar verdim.
Öncelikle bilmemiz gereken temel bilgiler;
MODULLER
FS içerisinde değişik modüller vardır. Bunlar simülatorun değişik görevlerini üstlenmişlerdir. Örneğin Sim1.dll, tamamen simülasyon kısmı, weather.dll meteorolojik koşullar üzerinde işlem yapar vb... Bu modüller MODULES klasörü altında yer alır. Aslında her bir modul bildiğimiz DLL dosyalarıdır. Gerçek anlamda bir add-on yazacak isek, bizimde aslında bir modul hazırlamamız ve bunu bir şekilde FS'e tanıtmamız gerekmekte.
VARIABLES
Biraz önce sözünü ettiğimiz her bir modul, kendi içerisinde bir takım değişkenler kullanır. Aynen bir program yazarken kullandığımız gibi. Bu değişkenlerin bir kısmı sadece o modül içerisinde kullanılır, bir kısmıda diğer modullerin kullanımına da açıktır. Bu değişkenleri kabaca TOKEN_VARIABLES olarak isimlendirebiliriz. Her bir değişkenin bir ID'si vardır ve bu ID ile o değişlkenin değerine kendi yazdığımız program içerisinden ulaşabilir ve değerini okuyabiliriz.
Tüm kullanıma açık olan 1000 civarındaki değişkeni ve ID değerlerini EXCELL dosyası olarak ekliyorum. Ayrıca gene ekte bulunan örnek DELPHI projesi içerisine yazdığım unit içerisinde bu TOKEN VARIABLE tanımları mevcut.
KEY_EVENTS
FS'e verilebilecek her türlü girdiyi KEY_EVENT olarak tanımlarız. Örneğin FLAP açma, IŞIK yapma söndürme vb. birer KEY EVENT'dir ve aynen TOKEN_VARIABLE's da olduğu gibi ID Leri vardır. Bu ID leri gene ekte EXCEL olarak iletiyorum. Aynı şekilde DELPHI projesi içerisinde de mevcut. Yazdığımız program ile istediğimiz bir KEY_EVENT'i FS'e göndermemiz mümkün.
FSUIPC
Aslında FSUIPC, yukarıda belirttiğim VARIABLES değerlerini okuyan, ve dışarıdaki bir uygalamaya aktarabilen, aynı şekilde dışarıdaki bir uygulamadan gelen bilgiyi KEY_EVENT ve başka yöntemler ile tekrar FS içerisine atabilen bir moduldur. Zaman içerisinde çok gelişmiştir, ancak ana fonksiyonları değişmemiştir. FSUIPC dış bir uygulama ve FS arasında tampon gibi bir görev üstlenmiştir. Aynı şekilde, FS içerisinde çalışan bir uygulama da bu VARIABLES ve KEY_EVENTS lerle uğraşmamak için FSUIPC üzerinden de FS'le haberleşebilir. Buna örnek olarak PMDG yi gösterebiliriz. FS içerisinde çalışan bir uygulama olduğu halde FSUIPC gerektirir.
NOT: Bir başka yazıda aslında daha kolay olan Delphi+FSUIPC ile external add-on yazma üzerine örneklemeler yapabiliriz. Ben bu örnek uygulamada FSUIPC kullanmadan doğrudan FS ile haberleşme üzerine örnekleme yapacağım. Tabiki bu yöntem daha zordur ancak performans avantajı ve FSUIPC gerektirmemesi benim tercih sebebimdir.
Şimdi kendi modülümüzü yazmaya hazırız.
Bir FS modülü yazarken en azından mutlaka bulundurmamız gereken iki fonksiyonumuz var. Ve bu iki fonksiyonu DLL'imiz export etmeli ki FS bu iki fonksiyona erişebilsin.
TModuleInit = procedure; stdcall;
TModuleDeinit = procedure; stdcall;
Zaten anlaşılabileceği gibi bu iki fonksiyondan ilki, FS Modulümüzü init ederken otomatik olarak çağrılıyor.
İkincisi de artık modülümüz kapatılacağı zaman çağrılıyor. Bu iki fonksiyon haricinde istediğimiz gibi kodlama yapmakta özgürüz.
Şimdi bu iki fonksiyonu export etmek için linkage adı altında bir yapı hazırlıyor ve bunu export ediyoruz.
MODULE_LINKAGE = packed record
ModuleID : Integer;
ModuleInit : TModuleInit;
ModuleDeinit : TModuleDeinit;
ModuleFlags : Cardinal;
ModulePriority : Cardinal;
ModuleVersion : Cardinal;
ModuleTable : Pointer
end;
Dikkat ederseniz bu yapıda, sözüne ettiğimiz iki fonksiyon, ve diğer bazı parametreler var.
ModuleID eğer modülümüze bir ID vereceksek kullanılan bir değer. Eğer 0 verirsek FS otomatik olarak bize bir ID atayacak.
ModuleVersion FS2004 için $0900 olmak zorunda.
ModuleTable pointer'i eğer modülümüz diğer modullerin kullanımı için bir takım değişkenler sunuyor ise kullanılan bir parametre, bunu doldurmamıza gerek yok zira böyle bir amacımız da olmayacak.
Ayrıca, modul yazarken, eğer diğer modullerden bir takım bilgiler alma ihtiyacımız olacaksa bunu baştan FS'e bildirmemiz gerekiyor. Bunun içinde aynen yukarıda olduğu gibi bir yapı hazırlıyor, içerisine ilgilendiğimiz modullerin ID değerlerini dolduruyor ve bunu FS'e export ediyoruz. Daha sonra, kodumuz artık bu modüllerden bilgi alabiliyor.
MODULE_IMPORT = packed record
ImpMain : TIMPORTSentry;
ImpSim1 : TIMPORTSentry;
ImpPanels : TIMPORTSentry;
ImpWindow : TIMPORTSentry;
ImpG2D : TIMPORTSentry;
ImpEOL : TIMPORTSentry;
end;
Yukarıdaki yapıda, ben FS Main, Sim1, Panels, Window, G2D modüllerinden bilgi alabileceğimi tanımladım. Bu yapıya da aşağıdaki atamaları yaptım.
ImportTable.ImpMain.fnID :=$01;
ImportTable.ImpMain.fnptr :=nil;
ImportTable.ImpSim1.fnID :=$19;
ImportTable.Impsim1.fnptr :=nil;
ImportTable.ImpPanels.fnID :=$0F;
ImportTable.ImpPanels.fnptr :=nil;
ImportTable.ImpWindow.fnID :=$0A;
ImportTable.ImpWindow.fnptr :=nil;
ImportTable.ImpG2D.fnID :=$0D;
ImportTable.ImpG2D.fnptr :=nil;
ImportTable.ImpEOL.fnID :=$00;
ImportTable.ImpEOL.fnptr :=nil;
Burada her modülün ID değerini tanımladım, son olarak $00 değerini verdim, ($00 ID li bir modül yoktur), bu değer artık listenin bittiğini gösteriyor.
Evet, şimdi Delphi ile FS için bir DLL yazmanın gereklerini yerine getirdik. Peki modulümüz ne yapacak. Bu detaya girmeden önce FS'in iç işleyişi ile ilgili bir küçük bilgi daha vermek istiyorum.
FS'de içeride çalışan birden çok döngü mevcut. Her bir döngüye chain adı veriliyor. Her bir chain değişik bir timing ile çalışıyor ve her bir chain değişik bir amaç için tanımlanmış durumda. Örneğin bir chain sadece 3D olarak ekranı tazelerken, bir başka chain, AI uçakların yerlerini hesaplıyor, bir başkası da hava durumunu hesaplıyor vb...
NOT: Burda programcılık ilgilendirmeyen bir bilgi eklemek gerekirse, örneğin FS'in ekran tazeleme limitlerini zorlamak, 3D haricindeki diğer chainlere çok az çalışma zamanı bırakıyor ve her ne kadar çok yüksek tazeleme oranı alsakta, simülasyon olması gerektiği kadar smooth çalışmıyor. Bu sebeple sistemin sunabildiği en yüksek FPS değerini ölçüp, bunun bir miktar altındaki bir değere fixlemek her zaman için daha rahat bir simulasyon deneyimi sunacaktır.
Konuya geri dönecek olursak, yukarıda sözünü ettiğimiz her bir chain'in bir ID'si vardır. Burada bizi ilgilendirebilecek bir kaç tanesini örneklemek gerekirse;
$22 Simulasyon Chain, yaklaşık saniyede 18 kere çalışıyor
$12 Ekran tazeleme Chain, FPS değerimiz ne ise saniyede o kadar çalışıyor vb ...
NOT: Bu bilgilerin bir çoğunu, FS için bir proje hazırlarken FS'i doğrudan DEBUG ederek çıkarmıştım. Bu sebeple tam listeler mevcut değil. Ancak belki WEB de konuyla ilgili daha detaylı bilgi bulunabilir.
Bu chain olaylarının bizi ilgilendiren bölümü ise şu. Kendi yazdığımız modül içerisinde herhangi bir fonksiyonu bu chainler içerisine dahil etme ve chain'den çıkarma şansımız var. Bu sayede örneğin herhangi bir fonksiyonumuzu simulasyon dongusu içerisine dahil edersek, FS her bir turda bizim fonksiyonumuzuda bir kere çağıracak ve burda istediğimiz haltı karıştırabileceğiz.

Ancak dikkat edilmesi gereken nokta, bu fonksiyonu çok dikkatli yazmamız gerektiği. Zira gereksiz işlemler, FS in performansını düşürecektir.
Peki bu işi nasıl yapacağız. Gene FS'i DEBUG ederken, FS'in kendi fonksiyonlarını nasıl chainlere dahil ettiğini inceledim ve bu iş ile ilgili iki adet FS fonksiyonun memorydeki yerini buldum. Bu iki fonksiyon FS9.exe içerisinde, $0438 ve $043C adreslerinde yatıyorlar. Aşağıdaki örnek DELPHI kodu, istediğimiz bir fonksiyonumuzu bu chainlere dahil etmek ve çıkarmak için kullanılabilir.
procedure RegisterCallBack(Func:pointer; ChainID:longword; Operation:integer);
type
TByteArray = array [0..$ffff] of byte;
PByteArray = ^TByteArray;
var
PointerImportArray:PByteArray;
temp:pointer;
RegFuncOffset:integer;
begin
// Register/Unregister Operation ...
if Operation=1
then RegFuncOffset:=$0438
else RegFuncOffset:=$043C;
// Get FS9 (Un)Register Function Address ...
PointerImportArray:=ImportTable.ImpMain.fnptr;
move(PointerImportArray^[RegFuncOffset],temp,4);
// Call (Un)Register Function ...
asm
push Func;
push ChainID;
call temp;
end;
end;
Son hazırlık olarak, bahsettiğimiz FS'in TOKEN_VARIABLE larını okuma ve FS'e KEY_EVENT göndermek için yazdığım iki fonksiyonu inceleyelim.
function GetTokenF(Token:longword):double;
var
temp:MODULE_VARf;
p:pointer;
begin
temp.id:=Token;
p:=@temp;
asm
PUSH p
MOV EDX,ImportTable.ImpPanels.fnptr
CALL EDX+152
PUSH p
MOV EDX,ImportTable.ImpPanels.fnptr
CALL EDX+160
end;
result:=temp.var_value;
end;
Bu fonksiyon görüldüğü üzere, Panels modülünden istediğimiz TOKEN_VARIABLE'a ait değeri okuyor.
procedure FSKeyEvent(Key:longword;Param:longword);
begin
asm
// Get Aircraft Object ...
MOV EDX,ImportTable.ImpSim1.fnptr
CALL EDX+$70
// Find Actual Address ...
MOV EDX, DWORD PTR DS:[EAX]
// Push Params ...
PUSH 0
PUSH Param
PUSH Key
PUSH EAX
// Call KeyEvent
CALL DWORD PTR DS:[EDX+$1C]
end;
end;
Bu fonksiyonda, sim1.dll'e bir KEY_EVENT gönderiyor...
Şimdi işin zevkli kısmı başlıyor. Hemen bir fonksiyon yazıyoruz, ve bu fonksiyonu ana simulasyon dongusune dahil ediyoruz. Tabiki bu dahil etme ve çıkarma işlemlerini biraz önce bahsettiğim ModuleInit,ModuleDeInit kısımlarında yapmak en mantıklısı.
procedure CallBack(P0,P1:longword); stdcall;
var
VertSpeed:double;
GearPos:double;
GroundAlt:double;
PlaneAlt:double;
PlaneAGL:double;
begin
// Bu fonksiyon artik FS'in standart döngüsü içerisinde çalisacak...
// Bize lazim olan FPM degeri ile GEAR pozisyonu ...
VertSpeed:=GetTokenF(TOKEN_VERTICAL_SPEED); // 1/256 m/sec olarak geliyor ...
VertSpeed:=VertSpeed * 3 * 60 / 256; // Yaklasik olarak ft/min yapmak için
GearPos:=GetTokenF(TOKEN_GEAR_HANDLE_POS); // Up:0
// Eger yaklasik 1500FPM üzerinde tirmaniyor ve GEAR hala down ise toplayalim ...
if (VertSpeed>1500) and (GearPos<>0) then
begin
FSKeyEvent(KEY_GEAR_UP,0);
end;
// BONUS: Eger AGL 1000 ft altinda ise ve -500FPM ile alçaliyorsak GEAR açalim ...
GroundAlt:=GetTokenF(TOKEN_GROUND_ALTITUDE); // 1/256 meter ...
PlaneAlt:=GetTokenF(TOKEN_PLANE_ALTITUDE); // meters ...
PlaneAGL:=(PlaneAlt-GroundAlt/256)*3; // Yaklasik olarak feet cinsinden yerden yükseklik...
if (VertSpeed<-500) and (GearPos=0) and (PlaneAGL<1000) then
begin
FSKeyEvent(KEY_GEAR_DOWN,0);
end;
end;
Bu kodu incelerken farkettiğimiz olabildiğince basit, kısa ve öz olduğu. Zira ağır bir kod FS performansını düşürecektir. İlk bölümde yapılan iş, TOKEN_VERTICAL_SPEED ve TOKEN_GEAR_HANDLE_POS değerlerini okumak, ve VerticalSpeed değerini m/saniye'den ft/min değerine çevirmek. Daha sonra istediğimiz şartlar oluşunca (1500fpm or more and GearDown) FS'e teker toplama komutu göndermek.
İkinci bonus kısmında da, eğer uçağın AGL değeri 1000ft altında ise ve tekerler hala açılmamışsa ve varyo -500 veya altında ise teker açmak. Bunu yapmak için öncelikle AGL değerine ihtiyacımız var. Ancak listeyi incelediğimizde FS'de AGL değeri olmadığını görüyüoruz. Hemen başka bir çözüm ile, uçağın bulunduğu noktadaki GroundAltitude değerini alıyor, ve uçağın deniz seviyesinden olan yüksekliği ile farkına bakıyoruz. Bu bize AGL değerini veriyor. Tabiki metre/feet çevrimlerini de yapıyoruz.
Ekteki örnekleri Delphi7 compile edebilirsiniz. Bu örneği istediğiniz gibi geliştirerek fantazi yapmak mümkün.
Denemek için DLL dosyasını modules altına kopyalayıp FS'i yeniden başlatın.
Kolaylıklar gelsin...
Kaynak Kodlar:
http://www.flythy.org/staff/ilker/Sampl ... pleDLL.rar
Derlenmiş DLL:
http://www.flythy.org/staff/ilker/Sampl ... LTest1.dll