Ил-2 Штурмовик: Битва за Британию. Скрипты. Скрипты для чайников. Часть 4 - Загрузка подмиссий — различия между версиями

Материал из АвиаВики
Перейти к: навигация, поиск
Строка 7: Строка 7:
 
:[http://www.sukhoi.ru/forum/attachment.php?attachmentid=142554&d=131833436523 Samples.zip]
 
:[http://www.sukhoi.ru/forum/attachment.php?attachmentid=142554&d=131833436523 Samples.zip]
  
In the last lesson we have so busy trying to load a single SubMission to 30sec and monitors all the vehicles destroyed using the method OnActorDestroyed () to count.
+
В последнем уроке мы научились загружать подмиссии с заданным интервалом времени и учитывать уничтоженные объекты с помощью метода '''OnActorDestroyed()'''.  
With this lesson we will randomly select and load each one of multiple submissions and. We are also building a small trap for the player, namely friendly vehicles. Is one of those destroyed by the player, the mission is a failure. The player will learn to look more closely and we will learn how the Army finds a membership Actors, then both of them what  . In addition, we no longer count the total number of vehicles but only those of the Enemy.  
+
  
So prepare for the first submissions, the primary mission remains unchanged. We open the SubMission from last time, change the vehicle and then saves the mission under another name. Here as an example once befriended our vehicle:  
+
В этом уроке мы научимся случайным образом выбирать и загружать подмиссии, а также подготовим небольшую ловушку для игрока в виде дружественной техники при уничтожении которой засчитывается поражение. Игроку предстоит быть более внимательным, а мы узнаем как задать принадлежность к армии для '''Actor '''. Кроме того в зачет уничтоженных целей будет идти только вражеская техника, в отличие от предыдущего урока.
 +
 
 +
Основную миссию оставляем без изменений, возьмем под-миссию из прошлого урока, поменяем в ней технику и сохраним под другим именем. Сделайте несколько таких подмиссий:
  
 
[[Файл:Sfdp4-1.jpg|800px]]
 
[[Файл:Sfdp4-1.jpg|800px]]
  
  
Overall, I've created the next from last time, three other submissions, including one friend with a vehicle.
+
Одну из подмиссий сделаем с дружественным транспортным средством
The code for this example mission
+
Код скрипта:
  
 
  using System;
 
  using System;

Версия 08:59, 12 октября 2011

Автор: FG28_Kodiak
Ссылка: Перейти (перевод Google)

Скачать оригиналы миссий:

Samples.zip

Скачать примеры с Sukhoi.ru:

Samples.zip

В последнем уроке мы научились загружать подмиссии с заданным интервалом времени и учитывать уничтоженные объекты с помощью метода OnActorDestroyed().

В этом уроке мы научимся случайным образом выбирать и загружать подмиссии, а также подготовим небольшую ловушку для игрока в виде дружественной техники при уничтожении которой засчитывается поражение. Игроку предстоит быть более внимательным, а мы узнаем как задать принадлежность к армии для Actor . Кроме того в зачет уничтоженных целей будет идти только вражеская техника, в отличие от предыдущего урока.

Основную миссию оставляем без изменений, возьмем под-миссию из прошлого урока, поменяем в ней технику и сохраним под другим именем. Сделайте несколько таких подмиссий:

Sfdp4-1.jpg


Одну из подмиссий сделаем с дружественным транспортным средством Код скрипта:

using System;
using maddox.game;
using maddox.game.world;
using System.Collections.Generic;
public class Mission : AMission
{
   enum MissionCondition {Neutral, Success, Failure}    
   MissionCondition AktuelleMissionCondition = MissionCondition.Neutral;
   AiAircraft PlayerPlane;
   const int MaxAnzahlFeindlicheFahrzeuge = 10;
   int Zeitspanne = 4000; 
   int AnzahlFeindlicheFahrzeuge = 0;
   int ZerstoerteZiele = 0;    
   public override void OnBattleStarted()
   {
       base.OnBattleStarted();
       MissionNumberListener = -1;
       PlayerPlane = (AiAircraft)GamePlay.gpPlayer().Place();
   }    
   private void serverMessage(string msg)
   {
       GamePlay.gpLogServer (null, msg, new object [] {msg});
   }    
   public override void OnTickGame()
   {
       if (AktuelleMissionCondition == MissionCondition.Neutral)
       {    
           if (Time.tickCounter() % 1000 == 0 && (AnzahlFeindlicheFahrzeuge < MaxAnzahlFeindlicheFahrzeuge))  //ca. alle 30sek die Karte laden
           {
               Zeitspanne += 1000;  // Bei jedem neuen Fahrzeug 30 sekunden zur GesamtZeit hinzu
               Random ZufaelligesFahrzeug = new Random();            
               switch (ZufaelligesFahrzeug.Next(1,5))
               {
                   case 1:
                       GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub1.mis");
                   break;
                   case 2:
                       GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub2.mis");
                   break;
                   case 3:
                       GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub3.mis");
                   break;
                   case 4:
                       GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub4.mis");
                   break;
                   default:
                       serverMessage("Mission nicht gefunden");
                   break;
               }
           }            
           if (Time.tickCounter() > Zeitspanne && AktuelleMissionCondition != MissionCondition.Success)     
           {
               GamePlay.gpHUDLogCenter("Sie haben "+ ZerstoerteZiele.ToString() + " von " + AnzahlFeindlicheFahrzeuge.ToString() + " Fahrzeugen zerstört" );
               AktuelleMissionCondition = MissionCondition.Success;
           }
       }
   }    
   public override void OnActorCreated(int missionNumber, string shortName, AiActor actor)
   {
       base.OnActorCreated(missionNumber, shortName, actor);        
       if (actor is AiGroundActor)
       {
           GamePlay.gpLogServer (null, "Fahrzeug: {0}\n", new object [] {(actor as AiGroundActor).InternalTypeName()});//Testmeldung            
           if (actor != null && actor.Army() == 1)   // 1 steht für Rote Seite 2 würde für Blaue Seite stehen
           {
               AnzahlFeindlicheFahrzeuge++;   // Wird nur gezählt wenn gegnerische Seite
           }          
           Timeout(75, () => {
               if (actor != null)
               { 
                   (actor as AiGroundActor).Destroy(); 
               }
           });
       }
   }    
   public override void OnActorDead(int missionNumber, string shortName, AiActor actor, System.Collections.Generic.List<DamagerScore> damages)
   {
       base.OnActorDead(missionNumber, shortName, actor, damages);        
       string KilledName;         
        KilledName = missionNumber.ToString()+ ":0_Chief";         
        if ((damages.Count != 0) && (PlayerPlane.Name().Equals(damages[0].initiator.Actor.Name())) && KilledName.Equals(actor.Name()))
        {             
            if (actor != null && actor.Army() == 2)   // 2 steht für Rote Seite 1 würde für Blaue Seite stehen
           {
               GamePlay.gpHUDLogCenter("Sie haben eines ihrer eigenen Fahrzeuge zerstört - <<<Mission fehlgeschlagen>>>" );
               AktuelleMissionCondition = MissionCondition.Failure;
           }
            ZerstoerteZiele++;
        }
   }
}


First, back in the mission globally valid between variables:

enum MissionCondition {Neutral, Success, Failure}
MissionCondition AktuelleMissionCondition = MissionCondition.Neutral;

As we once again have a termination condition, we use a lesson from the past already known enumeration

const int MaxAnzahlFeindlicheFahrzeuge = 10; - Since this time we want to consider only the total number of enemy vehicles, a more meaningful variable name was chosen.

int Zeitspanne = 4000; - With this variable we consider that the last vehicle still needs some time to find their way back down. This variable is later raised in the code for each newly added SubMission to 1000 because we can not know how many vehicles have made friends at the end of the road.

int AnzahlFeindlicheFahrzeuge = 0; - Here is the current number of enemy vehicles stored. The rest does not change the previous lesson.

OnBattleStarted() и serverMessage(string msg) also remain unchanged.


public override void OnTickGame()
{
   if (AktuelleMissionCondition == MissionCondition.Neutral)
   {    
       if (Time.tickCounter() % 1000 == 0 && (AnzahlFeindlicheFahrzeuge < MaxAnzahlFeindlicheFahrzeuge))  //ca. alle 30sek die Karte laden
       {
           Zeitspanne += 1000;  // Bei jedem neuen Fahrzeug 30 sekunden zur GesamtZeit hinzu
           Random ZufaelligesFahrzeug = new Random();        
           switch (ZufaelligesFahrzeug.Next(1,5))
           {
               case 1:
                   GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub1.mis");
               break;
               case 2:
                   GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub2.mis");
               break;
               case 3:
                   GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub3.mis");
               break;
               case 4:
                   GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub4.mis");
               break;
               default:
                   serverMessage("Mission nicht gefunden");
               break;
           }
       }        
       if (Time.tickCounter() > Zeitspanne)     
       {
           GamePlay.gpHUDLogCenter("Sie haben "+ ZerstoerteZiele.ToString() + " von " + AnzahlFeindlicheFahrzeuge.ToString() + " Fahrzeugen zerstört" );
           AktuelleMissionCondition = MissionCondition.Success;
       }
   }
}

In contrast, if the method was OnTickGame().

if (AktuelleMissionCondition == MissionCondition.Neutral) - is first queried first whether the mission is still neutral status. So the mission was already a failure by a friendly target to be destroyed, no new submissions are more loaded.

if (Time.tickCounter() % 1000 == 0 && (AnzahlFeindlicheFahrzeuge < MaxAnzahlFeindlicheFahrzeuge)) - The query is executed when are passed about 30sec and has already spawned the number of enemy vehicles have not reached the maximum number. If the query is so true can be the first time around 1000 increased the amount of Zeitspanne += 1000 as a one Zeitspanne = Zeitspanne + 1000 to write. The + = (just as there are -=, *= and / =) is shorthand for lazy writing (Ok not quite ).

Random ZufaelligesFahrzeug = new Random() - we hereby create a new object with the name ZufaelligesFahrzeug from the Random class, this class will put us through. Net is available and provides us with pseudo-random numbers. Pseudo-states under the same conditions will always produce the same numbers. But for our purposes, ranging from this type of random numbers (where there is more information about this.).

switch (ZufaelligesFahrzeug.Next(1,5))
{
    case 1:
        GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub1.mis");
    break;
    case 2:
        GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub2.mis");
    break;
    case 3:
        GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub3.mis");
    break;
    case 4:
        GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub4.mis");
    break;
    default:
        serverMessage("Mission nicht gefunden");
    break;
}

To determine what is loaded SubMission we use a switch .. case statement. It is checked whether the listed behind switch expression, either an integer or a string must be consistent with the stated case behind constants. This will in turn proceed therefore first checks whether the first case is true, then if the second, etc. You can in C # does not specify the area behind the case, things like first case .5: are not possible in C #. Makes a case for, all instructions are to break; executed. By default, you can define a branch which is executed in any case, if no case is true. But as we look at the switch -. Statement of our script to switch (ZufaelligesFahrzeug.Next(1,5))

With ZufaelligesFahrzeug.Next(..,..) we produce with the help of the Random object is an integer random number. The first value in parentheses indicates the minimum value, this is including the second value indicates the maximum value, this is exclusive, which means the maximum value is not generated by the random number generator, the minimum value on the other hand already. If you want to, for example, random numbers 1 to 10 would have. 

.Next(1,11) . In our example, we currently have 4 missions so we need the numbers from 1 to 4, so we need to define. Next (1.5). The random number was generated, compares the switch statement case all cases were found to the right and then loads the specified there SubMission. Had we accidentally. .Next(1,6) as specified random number range, would be produced also from time to time the number 5, but there is no case scenario, so there would be the default active area and this gives us a brief message in the chat bar.

if (Time.tickCounter() > Zeitspanne) - we check whether the time has already expired, if yes, we give a "winning message" and to evaluate AktuelleMissionCondition = MissionCondition.Success; and the mission a success.

public override void OnActorCreated(int missionNumber, string shortName, AiActor actor)
{
   base.OnActorCreated(missionNumber, shortName, actor);    
   if (actor is AiGroundActor)
   {
       GamePlay.gpLogServer (null, "Fahrzeug: {0}\n", new object [] {(actor as AiGroundActor).InternalTypeName()});//Testmeldung        
       if (actor != null && actor.Army() == 1)   // 1 steht für Rote Seite 2 würde für Blaue Seite stehen
       {
           AnzahlFeindlicheFahrzeuge++;   // Wird nur gezählt wenn gegnerische Seite
       }      
       Timeout(75, () => {
           if (actor != null)
           { 
               (actor as AiGroundActor).Destroy(); 
           }
       });
   }
}

OnActorCreated were added to the method following instructions:

GamePlay.gpLogServer (null, "Fahrzeug: {0}\n", new object [] {(actor as AiGroundActor).InternalTypeName()});

With the help of .InternalTypeName() can retrieve the output internal name of a game or AiGroundActors. The output looks like this:

Sfdp4-2.jpg


if (actor != null && actor.Army() == 1) - is queried with the If statement whether the actor is present and whether it belongs to the opposing army. Army() returns an integer value for the Army, this one stands in the Red Cliffs of Dover on page 2 and blue for the page. So the actor belongs to the Red side of enemy vehicles will be increased by one.

public override void OnActorDead(int missionNumber, string shortName, AiActor actor, System.Collections.Generic.List<DamagerScore> damages)
{
   base.OnActorDead(missionNumber, shortName, actor, damages);    
   string KilledName;     
   KilledName = missionNumber.ToString()+ ":0_Chief";     
   if ((damages.Count != 0) && (PlayerPlane.Name().Equals(damages[0].initiator.Actor.Name())) && KilledName.Equals(actor.Name()))
   {         
       if (actor != null && actor.Army() == 2)   // 2 steht für Blaue Seite
       {
           GamePlay.gpHUDLogCenter("Sie haben eines ihrer eigenen Fahrzeuge zerstört - <<<Mission fehlgeschlagen>>>" );
           AktuelleMissionCondition = MissionCondition.Failure;
       }
       ZerstoerteZiele++;
   }
}


Compared to the mission from the last lesson, this If statement was inserted:

if (actor != null && actor.Army() == 2)   // 2 steht für Blaue Seite
{
   GamePlay.gpHUDLogCenter("Sie haben eines ihrer eigenen Fahrzeuge zerstört - <<<Mission fehlgeschlagen>>>" );
   AktuelleMissionCondition = MissionCondition.Failure;
}

You from asking whether the actor is present and by actor.Army() == 2 if it belongs to the blue (ie our) side, this is the case, a message is displayed and updated mission set to failure condition, so the mission was a failure.

Sfdp4-3.jpg


When testing the above mission, is a relatively quick notice that some vehicles do not come to the second camouflage netting, before they disappear. These vehicles are too slow and therefore need more time until they reach their destination. One could, of course, now simply increase the time after which the vehicles Destroy () will be removed. But then there would be any traffic jams at the destination, which would be an easy target for the player.

To avoid this, you can also write a method that checks where an actor is straight and when this reaches a certain point, it is simply removed from the game. Such a method might look like this:

private void DestroyGroundActorAtPosition(double x, double y, double destroyRadius)
{
   Point3d DestroyPos;
   DestroyPos.x = x;
   DestroyPos.y = y;
   DestroyPos.z = 1;
   for (int i = 0; i < MissionsCount; i++)
   {
       AiGroundActor curActor;
       for (int j = 0; j < 10; j++)
       {
           string nameActor = i.ToString() + ":0_Chief" + j.ToString();
           curActor = GamePlay.gpActorByName(nameActor) as AiGroundActor;
           if (curActor != null)
           {
               if (curActor.Pos().distance(ref DestroyPos) < destroyRadius) 
               {   
                       curActor.Destroy();
               }
           }                             
       }
   }       
}

Let's look closer. .

private void DestroyGroundActorAtPosition(double x, double y, double destroyRadius) - As transfer values ​​required by this self-written method, the X and Y coordinates of the target point and a radius around the point, so that you can specify the distance to the point at which an actor can be ground away.

Point3d DestroyPos; - DestroyPos is declared as a Point3D, Point3D is an internal data structure of Cliffs of Dover. Access to this structure, we get the result we maddox.GP the namespace by using maddox.GP; load at the beginning of our script. Point3d contains 3 double values, one each for x, y and z are, therefore, all the information a localization in a 3rd dimensional space is required. DestroyPos is with. X = x and. Y = y by the method of transfer values ​​and through. Z = 1 determined by us, the Z-value we define by 1 because we want to disappear leaving only ground targets. To ensure that all actors can be considered from all these missions you will be using the method OnMissionLoaded (), these count from the game called whenever a mission is reloaded. First, we still need to introduce an additional global variable int count = 1 mission, this mission will be increased in OnMissionLoaded count by one.

public override void OnMissionLoaded(int missionNumber)
{
   base.OnMissionLoaded(missionNumber);
   MissionsCount++;           
}   

So back to DestroyGroundActorAtPosition is there with the help of a for - loop

for (int i = 0; i < MissionsCount; i++) The mission number incremented inside the loop then declares a new name with AiGroundActor curActor. Then we need a second loop for (int j = 0; j <10; j + +), with the help of two loops, we set then all currently possible Actress name by string name actor = i.ToString () + ": 0_Chief" + j. ToString (); together, it is necessary to always "caught" the Actor is correct. With curActor GamePlay.gpActorByName = (name actor) as AiGroundActor, we then assign the variables with the help of curActor gpActorByName. With if (curActor! = Null) then we check whether this exists and if Actor (curActor.Pos (). Distance (ref DestroyPos) <destroy radius), we note whether the actor is already close to our point. With curActor.Pos () we get the current location of our Actors supplied as Point3D, from this position we can determine distance (ref DestroyPos) the distance to our previously specified point. distance is a method of Point3D and expected as the transfer value is a reference to the target point. Is the actor close enough to the target point with the Actor Destroy () will be removed.

DestroyGroundActorAtPosition we bring the end of the method under OnTick DestroyGroundActorAtPosition (16761.94, 14817.99, 10.0), the XY values ​​I've taken from the submission file, the value of 10.0 for the distance from the target point is sufficient. DestroyGroundActorAtPosition we will be expanding in a later lesson, so that we "clean up" after using this method for a whole range of Actors can.

Here's the whole script:

using System;
using System.Collections;
using maddox.game;
using maddox.game.world;
using maddox.GP;
using System.Collections.Generic;
public class Mission : AMission
{
   enum MissionCondition {Neutral, Success, Failure}    
   MissionCondition AktuelleMissionCondition = MissionCondition.Neutral;
   AiAircraft PlayerPlane;
   const int MaxAnzahlFeindlicheFahrzeuge = 10;
   int Zeitspanne = 4000; // Zeitspanne bis letztes Fahrzeug sicher verschwunden ist.
   int AnzahlFeindlicheFahrzeuge = 0;
   int ZerstoerteZiele = 0;
   int MissionsCount = 1;    
   public override void OnMissionLoaded(int missionNumber)
   {
       base.OnMissionLoaded(missionNumber);
       MissionsCount++;           
   }
   public override void OnBattleStarted()
   {
       base.OnBattleStarted();
       MissionNumberListener = -1;
       PlayerPlane = (AiAircraft)GamePlay.gpPlayer().Place();
   } 
   private void serverMessage(string msg)
   {
       GamePlay.gpLogServer (null, msg, new object [] {msg});
   }    
   private void DestroyGroundActorAtPosition(double x, double y, double destroyRadius)
   {
       Point3d DestroyPos;
       DestroyPos.x = x;
       DestroyPos.y = y;
       DestroyPos.z = 1;
       for (int i = 0; i < MissionsCount; i++)
       {
           AiGroundActor curActor;
           for (int j = 0; j < 10; j++)
           {
               string nameActor = i.ToString() + ":0_Chief" + j.ToString();
               curActor = GamePlay.gpActorByName(nameActor) as AiGroundActor;
               if (curActor != null)
               {
                   if (curActor.Pos().distance(ref DestroyPos) < destroyRadius) 
                   {   
                           curActor.Destroy();
                   }
               }                             
           }
       }       
   }    
   public override void OnTickGame()
   {
       if (AktuelleMissionCondition != MissionCondition.Failure  && AktuelleMissionCondition != MissionCondition.Success)
       {    
           if (Time.tickCounter() % 1000 == 0 && (AnzahlFeindlicheFahrzeuge < MaxAnzahlFeindlicheFahrzeuge))  //ca. alle 30sek die Karte laden
           {
               Zeitspanne += 1000;  // Bei jedem neuen Fahrzeug 30 sekunden zur GesamtZeit hinzu
               Random ZufaelligesFahrzeug = new Random();            
               switch (ZufaelligesFahrzeug.Next(1,5))
               {
                   case 1:
                       GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub1.mis");
                   break;
                   case 2:
                       GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub2.mis");
                   break;
                   case 3:
                       GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub3.mis");
                   break;
                   case 4:
                       GamePlay.gpPostMissionLoad("missions\\Single\\Samples\\TestSubmissions\\MissionNachladen6Sub4.mis");
                   break;
                   default:
                       serverMessage("Mission nicht gefunden");
                   break;
               }
           }            
           if (Time.tickCounter() > Zeitspanne && AktuelleMissionCondition != MissionCondition.Success)     
           {
               GamePlay.gpHUDLogCenter("Sie haben "+ ZerstoerteZiele.ToString() + " von " + AnzahlFeindlicheFahrzeuge.ToString() + " Fahrzeugen zerstört" );
               AktuelleMissionCondition = MissionCondition.Success;
           }
       }    
       DestroyGroundActorAtPosition(16761.94, 14817.99, 10.0);        
   }    
   public override void OnActorCreated(int missionNumber, string shortName, AiActor actor)
   {
       base.OnActorCreated(missionNumber, shortName, actor);        
       if (actor is AiGroundActor)
       {
           GamePlay.gpLogServer (null, "Fahrzeug: {0}\n", new object [] {(actor as AiGroundActor).InternalTypeName()});//Testmeldung            
           if (actor != null && actor.Army() == 1)   // 1 steht für Rote Seite 2 würde für Blaue Seite stehen
           {
               AnzahlFeindlicheFahrzeuge++;   // Wird nur gezählt wenn gegnerische Seite
           }
       }
   }    
   public override void OnActorDead(int missionNumber, string shortName, AiActor actor, System.Collections.Generic.List<DamagerScore> damages)
   {
       base.OnActorDead(missionNumber, shortName, actor, damages);        
       string KilledName;         
       KilledName = missionNumber.ToString()+ ":0_Chief";         
       if ((damages.Count != 0) && (PlayerPlane.Name().Equals(damages[0].initiator.Actor.Name())) && KilledName.Equals(actor.Name()))
       {             
           if (actor != null && actor.Army() == 2)   // 2 steht für Blaue Seite
           {
               GamePlay.gpHUDLogCenter("Sie haben eines ihrer eigenen Fahrzeuge zerstört - <<<Mission fehlgeschlagen>>>" );
               AktuelleMissionCondition = MissionCondition.Failure;
           }
           ZerstoerteZiele++;
       }
   }
}


Для установки миссий-примеров скопируйте их в папку:
..\Documents\1C SoftClub\il-2 sturmovik cliffs of dover\missions\Single

Загрузка подмиссий (автор FG28_Kodiak)

Оригинальные темы см. на форуме sturmovik.de(перевод от Google):