Scripting Task - Random Car Liveries - Part 1

pcas1986

Well-known member
One of my Trainz activities is writing scripts, sometimes for myself, but often for others. I'm rather choosy about this and only take on scripting tasks for others if the topic interests me. Generally this will be some aspect of Trainz script I have not done before. One of my preferred customers, Kieran AKA CaptainKMan, makes some unique assets so I frequently find myself helping him out.

The purpose of this particular series of blog articles is to describe my script development process so that others might learn from it. Or learn from my mistakes. :hehe:


On this particular occasion, Kieran has built two assets: a 1963 Volkswagen "Beetle" as a product and the same vehicle as traffic (carz). But, instead of having the same coloured vehicle each time, he has asked that the vehicle colour scheme be random and chosen from a texture library. So my task is to create a script for both assets and provide any changes necessary to the asset config.txt. For this Kieran has provided me with a CDP of the assets plus a couple of "helper" assets.

The target Trainz version is TS09 but I am developing the script in TS10 as it is more convenient.

Before starting a scripting task it is useful to have a test scenario. In programmer parlance this is akin to a test harness. Kieran has a flat car asset (kuid:459464:100007) that has, by default, a product called Mr Bean's Mini Product (kuid:459464:1479). Here is a small consist in Surveyor with my GWR 54XX Tank Loco, the flat car loaded with three Mr Bean Minis and a version of Andi Smith's Loco Shed.


pcas1986_20121128_0000.jpg


The Mr Bean Mini Product is not the focus of the script task but serves to demonstrate that three of the same coloured vehicle is undesirable.


So, why is Andi Smith's Loco Shed in the Session?

Actually this particular asset is the result of Andi Smith's very clever scripting tutorial at http://online.ts2009.com/mediaWiki/index.php5/Getting_Started_in_TrainzScript. That asset demonstrates the use of texture replacement using a texture library and a properties browser. For our solution we don't need the properties browser but there is code in the script that can be re-used for our task.

Here is the Loco Shed script:

Code:
include "Buildable.gs"
include "Interface.gs"


/*  A Loco Shed with opening doors, flashing coronas, changeable shed name, changeable wall texture, removable roof, trigger points, track attachments, saving configuration
    to the Trainz database (soup) and a Property Browser variant
*/


class Tutorial isclass Buildable {


   bool doorsOpen = false;
   bool roofVisible;
   string nameText = "None";








// Global variables to hold the texture library, skin index and description.
   Asset skins;
   int skin;
   string skinDescription = "None";


   void SetNameText(void);


   public void Init(void) {
      inherited();
      SetFXCoronaTexture("corona",null);
      SetNameText();
      AddHandler(me,"Object","","ObjectHandler");
// This call sets skins, to refer to the texture-group defined in our kuid-table.
      skins = GetAsset().FindAsset("skins");
   }


   Asset GetCorona(string mesh, string effect) {
      Soup meshtable = GetAsset().GetConfigSoup().GetNamedSoup("mesh-table");
      Soup effects = meshtable.GetNamedSoup(mesh).GetNamedSoup("effects");
      KUID kuid = effects.GetNamedSoup(effect).GetNamedTagAsKUID("texture-kuid");
      return World.FindAsset(kuid);
   }


   thread void RingTheBell(void) {
      while (doorsOpen) {
         Sleep(0.35 + World.Play2DSound(GetAsset(),"bell.wav"));
      }
   }


   void SetNameText(void) {
      if (nameText == "None" or nameText == "") {
         SetFXNameText("name"," ");
      } else {
         SetFXNameText("name",nameText);
      }
   }


   /* SetSkin() is a new method to deal with executing the texture-replacement
   calls. If the value of the skin parameter is zero then texture replacement
   will be turned off and the model will revert to the texture initially defined 
   in the mesh.  Any other value will result in the corresponding texture from 
   the texture-group being assigned to the mesh. */
  
   void SetSkin(int skin) {
      if (skin == 0) SetFXTextureReplacement("masonry",null,0);
      else SetFXTextureReplacement("masonry",skins,skin);
   }


   void ObjectHandler(Message msg) {
      Vehicle vehicle = cast<Vehicle>msg.src;
      if (!vehicle) return;
      if (msg.minor == "InnerEnter") {
         doorsOpen = true;
         SetMeshAnimationState("default", true);
         SetFXCoronaTexture("corona",GetCorona("default","corona"));
         RingTheBell();
      }
      else if (msg.minor == "InnerLeave") {
         doorsOpen = false;
         SetMeshAnimationState("default", false);
         SetFXCoronaTexture("corona",null);
      }
   }


   /* GetNamedTagAsInt() allows us to initialise skin with a default value
   if the tag doesn't already exist.  GetNamedTag() retrieves a string value
   and doesn't have the same capability, so we have to do a little additional
   work to ensure that skinDescription is always initialised. If you look
   back at the top of the file you will see that we assigned the value of 
   "None" to skinDescription when it was originally declared.  We leave 
   that value alone unless soup has anything more interesting to tell us. */


   public void SetProperties(Soup soup) {
      inherited(soup);
      roofVisible = soup.GetNamedTagAsBool ("roof",true);
      SetMeshVisible("roof",roofVisible,1.0);
      //nameText = soup.GetNamedTag("name");
      nameText = GetLocalisedName();
      SetNameText();
      skin = soup.GetNamedTagAsInt("skin",0);
      string temp = soup.GetNamedTag("skinDescription");
      if (temp != "") skinDescription = temp;
      SetSkin(skin);
   }


   public Soup GetProperties(void) {
      Soup soup = inherited();
      soup.SetNamedTag("roof",roofVisible);
      soup.SetNamedTag("name",nameText);
      soup.SetNamedTag("skin",skin);
      soup.SetNamedTag("skinDescription",skinDescription);
      return soup;
   }


/* Additional code to present skinDescription in the Property Editor. */


    public string GetDescriptionHTML(void) {
      string roofStatus = "Show";
      if (roofVisible) roofStatus = "Hide";
      string html = inherited()
         + "<font size=5><br>"
         + "Roof: <a href=live://property/roof>" + roofStatus + "</a><br>"
         + "Name: <a href=live://property/name>" + nameText + "</a><br>"
         + "Name: <a href=live://property/skin>" + skinDescription + "</a><br>"
         + "</font>";
      return html;
   }


   public string GetPropertyName(string pID) {
      string result = inherited(pID);
      if (pID == "name") result = "Enter Name Text";
      else if (pID == "skin") result = "Select Masonry Texture";
      return result;
   }
 
/* Advise Trainz that the skin property is of type "list". */


   public string GetPropertyType(string pID) {
      string result = inherited(pID);
      if (pID == "roof") result = "link";
      else if (pID == "name") result = "string";
      else if (pID == "skin") result = "list";
      return result;
   }


/* GetPropertyElementList() builds the list that we will be presenting to the 
   users to allow them to select a texture. The method returns an array, or 
   indexed list, of strings.  It does this by loading the names of the skins 
   from the textures table in config.txt of the texture group. This table 
   lists all of the *.texture files which the asset contains. The method 
   then parses through the soup using a for loop and chops the ".texture" 
   off the end of the filename before adding it to our result array. */


   public string[] GetPropertyElementList(string pID) {
      string[] result = inherited(pID);
      if (pID == "skin") {
         result[result.size()] = "None";
         Soup soup = skins.GetConfigSoup().GetNamedSoup("textures");
         int n;
         for (n = 1; n < soup.CountTags(); n++) {
            result[result.size()] = Str.Tokens(soup.GetNamedTag(n),".")[0];
         }
      }
      return result;
   }


   public string GetPropertyValue(string pID) {
      string result = inherited(pID);
      if (pID == "name") result = nameText;
      return result;
   }


/* You may have become used to simply adding new statements to the standard property
   methods, but this is a bit of a special case. SetPropertyValue() is a new
   function with the same name as a method which already exists but with a different
   set of parameters. TrainzScript allows this duplication and it can be very useful,
   it can also be a little confusing, so you need to watch out for it.


   In this particular case we want the same method to deal with two values:
   the skin index and the skin description. The first is a number representing the
   position of the selection within the list, and the second is the string value of
   the list item itself. Both are set in the same method before calling SetSkin()
   which actually does the skin swapping. */


   public void SetPropertyValue(string pID, string value, int index) {
      if (pID == "skin") {
         skin = index;
         skinDescription = value;
         SetSkin(skin);
      }
      else inherited(pID,value,index);
   }


   public void SetPropertyValue(string pID, string value) {
      if (pID == "name") {
         if (value == "") nameText = "None";
         else nameText = value;
         SetNameText();
      }
      else inherited(pID, value);
   }


   public void LinkPropertyValue(string pID) {
      if (pID == "roof") {
         roofVisible = !roofVisible;
         SetMeshVisible("roof",roofVisible,1.0);
      }
      else inherited(pID);
   }


};

In Part 2 I will discuss my test asset and initial experiments.
 
You forgot the scenery version of the bug. :)

I should have set the Mini to only load one on the single attachment....what was I thinking! And I need to make the bug use 3 attachments for 3 individual cars. When i made the flat wagon (ok, modified, it's a modification of one by Alberte Zato) the '63 Bug product was only Herbie, but with no other colours. When i made the driveable version with different textures, I wondered if the same could be applied to other versions of Herbie, and so I asked for a script.

I'm interested to see how this progresses. Well, I ought to be! :D
 
Well, one thing at a time. It takes time to get my head around this problem and understanding the requirements can be difficult. Often the customer doesn't always know themselves - that's normal! Often, as the program develops, new requirements are identified and that's fine.

Not sure I understand your comment about the Mini. The browser only allows three Minis - I assumed that was because of the size. I did try the Beetle but it only loaded one and that was sideways which I think I mentioned elsewhere.

The flat car is fine as I'm only using it as a test asset that uses products but if you want to change it or substitute another that's ok as well.
 
To clarify:

Since the mini has only one texture I should have set it in the queue that allows only one, and I should have moved the Beetle into the queue that allows three.

I didn't know the beetle was sideways. Mine works just fine. I'll address that problem later though, before I release everything.

Now, back to my flat wagon tweaking and industry making. :)
 
I'm curious why your beetle was straight and mine was not. There must be something different in the setup, or version or the phase of the moon! I'm now going to start on the product version of my script given the encouraging results of the loco shed. See the second blog article on that.

One of these days I plan to make an industry but something weird with lots of moving parts, puffs of smoke and funny noises.
 
Back
Top