Prequel to Andi Smith's wiki tutorial shed house tutorial script
From TrainzOnline
Back to: Prequel_Blender_to_Trainz#Intro
include "Buildable.gs" /* Engineshed Tutorial Asset Prequel Blender ----------------------------------------- Step 5: Setting up the asset Step 6: Automating door animations Step 7: Handling corona effects Step 8: Playing sound Step 9: Hiding meshes Step 10: Handling name effects Step 11: Texture replacements */
class Tutorial isclass Buildable {
/* Step 6: We will need a variable to keep track of whether the shed doors are open or closed, declaring this in the body of the class and outside of any method definitions will make it available to any method in the class. */ bool doorsOpen = false; // Step 9: Variable to knowing about if the roof is set visible or not bool roofVisible = true;
/* Step 10: It is legitimate to assign an initial value when a variable is declared. The value can be still altered by other code statements. Becaus the property browser has within links problems with empty ot blank strings (no clickable text) we workaround with a html enforced blank. And we are going to be calling a new method called SetFasciaText() in our Init() method. The method itself will be defined later on but the script will not compile unless it knows the parameters required by the method when it first finds a reference to it. To get around this we can predefine it in the body of the class. */ string fasciaText = " "; void SetFasciaText(void);
/* Step 11: Global variables to hold the texture library, skin index and description. */ Asset skins; // Texture library asset object int skin; // ID-number of the current skin string skinDescription = "Original Texture"; bool isConfigSoupCached = false; // Is the config soup cach ready? int errCodeConfigCaching = 0; // Used to decide browser text variant thread void asyncConfigSoupCache(Asset asset);
/* Step 5: Create the mandantory init-method Step 6: We are going to open and close the doors whenever a vehicle enters or leaves the area defined by our trigger. To do this we will be listening for Object,InnerEnter and Object, InnerLeave messages. We do this by adding a handler in Init(). */ public void Init(void) { inherited(); // Step 5: Switch corana light off because its initial on SetFXCoronaTexture("corona",null); // Step 10: Setting fasciaText to signboard at the shed gable SetFasciaText(); // Step 6: Registering a trigger-handler AddHandler(me,"Object","","ObjectHandler"); // Step 11: This call sets skins, to refer to the texture-group defined in // our kuid-table. And then start the async caching of the config soup // with skin-list inside it. skins = GetAsset().FindAsset("skinskuid"); asyncConfigSoupCache(skins); }
/* Step 11: The async thread method to cach the config-soup of the texture- library with error checking */ thread void asyncConfigSoupCache(Asset asset) { AsyncQueryHelper queryHelper = asset.CacheConfigSoup(); Sniff(queryHelper, "AsyncQuery", "", true); Message msg; wait() { on "AsyncQuery", "", msg: if (msg.src == queryHelper) { break; } continue; } int errCode = queryHelper.GetQueryErrorCode(); if (queryHelper.IsErrorCode(errCode)) { Interface.Log("ConfigCaching - cannot load config - errcode = " + errCode); errCodeConfigCaching = errCode; return; } isConfigSoupCached = true; }
/* Step 7: GetCorona is a utility method that will read the texture-kuid tag from the config.txt of our shed. When we turned the corona off in Init() the reference to the KUID of the corona was lost and there is no easy way to reset it. Thismethod retrieves the value and returns it as a corona asset. Code which reads datafrom a Soup, which is the Trainz word for database, is often very convoluted and difficult to follow. I have split the code into different calls here, and made it a separate method in order to keep it readable, but it could have been writtenas one long statement. */ // TBD: Question to proof: Is it possible to store it in a variable? 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); }
/* Step 7: We can use the same mechanism to control the coronas that we are using to open and close the shed doors, we just need to add the appropriate statements. Step 8: This is the bell ringing method, the keyword thread signifies that it is to run independently of any other code. You will remember our global variable, doorsOpen which is set to true or false in the ObjectHandler method. RingTheBell uses a while loop to monitor this variable and will keep running until doorsOpen becomes false. World.Play2DSound() plays a non-positional sound, that is it can be heard anywhere on the route. I've added a call to Sleep after the sound is played, enough time to allow the sound to play, plus a short pause. On the next loop the method will check to see if the doors are still open and run again if so. Once they have closed the while loop and the method will end. All of this runs independently of other parts of the class code. */ thread void RingTheBell(void) { while (doorsOpen) { World.Play2DSound(GetAsset(),"bell.wav"); Sleep(1.5); } }
/* Step 10: This is the actual declaration of SetFasciaText(). We are going to use this method to work around the null string and blank parameters problem with the browser link. Since this hack will be needed several times we have 'centralised' it here. */ void SetFasciaText(void) { string text = Str.CloneString(fasciaText); Str.TrimLeft(text,""); if (text == "") SetFXNameText("name"," "); // Show a blank signboard else SetFXNameText("name",fasciaText); }
/* Step 11: 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); }
/* Step 6: Object handler will 'wake up' whenever we receive an Object message. It is good practice to check that the message is coming from an expected source. This is done by converting or 'casting' the source of the message to a vehicle.If the cast does not work the variable vehicle will be null and the handler is exited immediately. The remainder of the method checks whether the vehicle is entering or leaving and runs the animations as appropriate. SetFXAnimationState("xxx", false) set the attached animation effect xxx inactiv (restet to frame 0) and SetFXAnimationState("xxx", true) set the animation effect xxx activ and runs the attached animation. Step 8: When the doors are opened we call the thread RingTheBell to start the bell ringing. There is no need to stop it, this will happen automatically. */ void ObjectHandler(Message msg) { // Step 6: Casting let us know about that no other kind of // object has triggered the trigger, only a vehicle. Vehicle vehicle = cast<Vehicle>msg.src; if (!vehicle) return; if (msg.minor == "InnerEnter") { doorsOpen = true; SetFXAnimationState("closedoors", false); SetFXAnimationState("opendoors", true); // Step 7: Switch the light over the doors on SetFXCoronaTexture("corona",GetCorona("shed-lod0","corona")); // Step 8: Ring the bell RingTheBell(); } else if (msg.minor == "InnerLeave") { doorsOpen = false; SetFXAnimationState("opendoors", false); SetFXAnimationState("closedoors", true); // Step 7: Switch the light over the doors off SetFXCoronaTexture("corona",null); } }
/* Step 9: SetProperties() is another method which is called by the game engine. Its purpose is to read the session files and to provide you with the opportunity to retrieve any data which has been saved there in previous runs. The method is also called when the Property Editor is closed, to save any changes which have been made. The Undo system also uses this method. We are using it here to restore any value for the roof visibility which has previously been saved by the game. There will be nothing there of course, when the asset is first placed. Step 10: First time call of GetNamedTag there is a "" string result, if the tag doesn't exist. So we have to test it to not loose the fasciaText value. Step 11: 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 "Original Texture" 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-lod0",roofVisible,1.0); // Step 10: Additional code to retrieve saved values for fasciaText. string text = soup.GetNamedTag("name"); Str.TrimLeft(text,""); if (text!="") fasciaText=Str.CloneString(text);
else fasciaText=" ";
SetFasciaText(); // Step 11: Additional code skin = soup.GetNamedTagAsInt("skin",0); string temp = soup.GetNamedTag("skinDescription"); if (temp != "") skinDescription = Str.CloneString(temp);
else skinDescription = "Original Texture";
SetSkin(skin); }
/* Step 9: GetProperties() is again called by the game engine, this time it's used to take any changes made by the user and to save them to the session files. */ public Soup GetProperties(void) { Soup soup = inherited(); soup.SetNamedTag("roof",roofVisible); // Step 10: Additional code to save the present value of fasciaText. soup.SetNamedTag("name",fasciaText); // Step 11: Additional code soup.SetNamedTag("skin",skin); soup.SetNamedTag("skinDescription",skinDescription); return soup; }
/* Step 9: GetDescriptionHTML() is where you define the HTML code that will be loaded into the Property Editor when you press [F3] in Surveyor, select the [?] icon, and click on the Engine Shed. The HTML code supported is very simple. You just need to provide the basics. The local variable roofStatus is used to provide text to be used by the interface to show whether the roof is to be hidden or displayed. <a href=live://property/roof> is a special hyperlink format used by Trainz to identify a link clicked within the Property Editor. The 'roof' part, known as a propertyID, is used by other related functions to identify values that the user wants to modify. Step 10: Additional code to present fasciaText in the Property Editor. Step 11: Additional code to present skinDescription in the Property Editor. First we decide the browsertext to show. If config soup is cachhed without error, we use the property-live-link. Otherwise if error code is 0, we tell to try later because the caching isn't finished and otherwise we tell about caching is failed. */ public string GetDescriptionHTML(void) { string roofKlickAction = "Show roof"; if (roofVisible) roofKlickAction = "Hide roof"; string selectedSkinText = ""; if (isConfigSoupCached) selectedSkinText = "<a href=live://property/skin>" + skinDescription + "</a><br>"; else if (errCodeConfigCaching == 0) selectedSkinText = "Caching yet activ. Try later again."; else selectedSkinText = "Caching failed with errorcode " + errCodeConfigCaching; // Handle special characters in fasciaText with Quote method and blank string showText = ""; if (fasciaText == " ") showText = " "; else showText = BrowserInterface.Quote(fasciaText); string html = inherited() + "<font size=5>" + "Roof visibility: <a href=live://property/roof>" + roofKlickAction + "</a><br>" + "Gable labeltext: <a href=live://property/name>" + showText + "</a><br>"+ "Selected skin: " + selectedSkinText + "</font>"; return html; }
/* Step 10: When we edit fasciaText Trainz will provide a separate dialogue box that we can type into. This method requests a caption for that dialogue. So "GetPropertyName" is in the meaning of "get caption text of the input window". */ public string GetPropertyName(string pID) { string captiontext = inherited(pID); if (pID == "name") captiontext = "Enter Textstring"; // Step 11: Additional code for skin list editor. else if (pID == "skin") captiontext = "Select Masonry Texture"; return captiontext; }
/* Step 10: When we edit fasciaText Trainz will provide a separate dialogue box that we can type into. This method requests a additional tooltip information for that dialogue. So "GetPropertyDescription" is in the meaning of "get the text of the additional tooltip for the input window". */ public string GetPropertyDescription(string pID) { string description = inherited(pID); if (pID == "name") description = "" + "The entered text string will be shown " + "at the top of the door gable of the shed."; // Step 11: Additional code for skin list editor tooltip. if (pID == "skin") description = "" + "Choose the skin from list"; return description; }
/* Step 9: The remaining methods are all standard routines called by the game to establish what needs to be done in response to a user click on any of the links in the Property Editor. GetPropertyType() establishes whether the property, "roof" in our case, is to be treated as a number, as a string value or is to be selected from a list. Roof represents the simplest property type, where merely clicking on the link is enough information for the game to do what needs to be done. We indicate this to Trainz by returning the string "link". */ public string GetPropertyType(string pID) { string result = inherited(pID); if (pID == "roof") result = "link"; // Step 10: Advise Trainz that the name property is of type "string". if (pID == "name") result = "string"; // Step 11: Advise Trainz that the skin property is of type "list". if (pID == "skin") result = "list"; return result; }
/* Step 11: 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") { // Step 11: Because GetConfigSoup is obsolete this is exchanged with a // async caching of the config soup. // Soup soup = skins.GetConfigSoup().GetNamedSoup("textures"); Soup soup = skins.GetConfigSoupCached().GetNamedSoup("textures"); int n; for (n = 0; n < soup.CountTags(); n++) { result[result.size()] = Str.Tokens(soup.GetNamedTag(n),".")[0]; } result[result.size()] = "Original Texture"; } return result; }
/* Step 11: 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 = Str.CloneString(value); SetSkin(skin); } else inherited(pID,value,index); }
/* Step 10: GetPropertyValue() enters the current value of fasciaText into a dialogue to allow it to be edited. */ public string GetPropertyValue(string pID) { string result = inherited(pID); if (pID == "name") result = fasciaText; return result; }
/* Step 10: SetPropertyValue() takes our edited value and assigns it to our fasciaText variable. This will only happen if we actually change the value in the TRS dialogue. Otherwise the original value will not be altered. */ public void SetPropertyValue(string pID, string value) { if (pID == "name") { // Proof if value is white space string string temp = Str.CloneString(value); Str.TrimLeft(temp,""); if (temp == "") fasciaText = " "; else fasciaText = Str.CloneString(value); SetFasciaText(); } else inherited(pID, value); }
/* Step 9: LinkPropertyValue() is where we tell the game what is to be done when properties which have been defined as "link" are pressed. In our case we take the boolean value roofVisible and make it the opposite of itself, toggling its value. (The symbol ! means not). Having done this we can use the new value of roofVisible as a parameter to SetMesh() which hides or shows the submesh. The final parameter of SetMeshVisible() specifies the time to fade the mesh in or out. */ public void LinkPropertyValue(string pID) { if (pID == "roof") { roofVisible = !roofVisible; SetMeshVisible("roof-lod0",roofVisible,1.0); } else inherited(pID); }
};
Back: Prequel_Blender_to_Trainz#Intro