HowTo/Upgrade GameObject.GetName()

From TrainzOnline
< HowTo
Revision as of 15:37, 3 March 2022 by Pw3r (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

The following is an example of how to update a script which uses the obsolete script function GameObject.GetName(). It is intended to be read by content creators already familiar with TrainzScript, but perhaps unfamiliar with the more modern concepts involved with Asynchronous Route Streaming.

It is recommended readers first familiarise themselves with topics covered in the following pages:


Contents

Overview

The script function GameObject.GetName() returns a string containing the objects 'script name'. This name is generally considered constant for any given GameObject in the world, meaning it persists between runs of the game and is suitable for long term storage, multiplayer network transmission, etc. The name can also be generally considered unique, but this isn't technically guaranteed.

With the introduction of Route streaming the script name is considered obsolete though, and should be replaced with GameObjectID.

Getting an Object ID

The following example script shows a simple class which gets and stores objects script name.

class Example
{
  string          m_gameObjectName;
  
  public void SetObject(GameObject obj)
  {
    // Save the objects ID.
    if (obj)
      m_gameObjectName = obj.GetName();
    else
      m_gameObjectName = "";
  }
};

This class has a single member variable used to store the name of an object, and a function which sets the object reference. To upgrade the script to support modern versions of Trainz, the name must be replaced with a GameObjectID, and the GetName() call replaced with GetGameObjectID().

class Example
{
  GameObjectID    m_gameObjectID = null;
  
  public void SetObject(GameObject obj)
  {
    // Save the objects ID.
    if (obj)
      m_gameObjectID = obj.GetGameObjectID();
    else
      m_gameObjectID = null;
  }
};

Getting an Object from an ID

The above example is a demonstration only, and serves no useful purpose. To give it some purpose, let's consider a class which sets a junction direction. In older versions of Trainz this was quite simple, as the object was more or less guaranteed to remain loaded forever.

class Example
{
  string          m_junctionName;
  
  public void SetObject(GameObject obj)
  {
    // Save the objects ID.
    if (obj)
      m_junctionName = obj.GetName();
    else
      m_junctionName = "";
  }
  
  public bool SetJunctionDirection(SecurityToken token, int direction)
  {
    if (m_junctionName == "")
      return false;
    
    Junction junc = cast<Junction>(Router.GetGameObject(m_junctionName));
    if (!junc)
      return false;     
    
    return junc.SetDirection(token, direction);
  }
};

Now we have a potentially useful helper class. However, the approach will not work in TRS19 and later, because Route streaming means the object may be unloaded and the relevant functions are declared obsolete. As such, we must instead use the GameObjectID, as follows:

class Example
{
  GameObjectID    m_junctionID = null;
  
  public void SetJunction(Junction junc)
  {
    // Save the junctions ID.
    if (junc)
      m_junctionID = junc.GetGameObjectID();
    else
      m_junctionID = null;
  }
  
  public bool SetJunctionDirection(SecurityToken token, int direction)
  {
    if (!m_junctionID)
      return false;
    
    Junction junc = cast<Junction>(Router.GetGameObject(m_junctionID));
    if (!junc)
      return false;     
    
    return junc.SetDirection(token, direction);
  }
};

This approach is a direct replacement to the original functions, but if Route streaming is enabled and the junction has been unloaded, then a call to SetJunctionDirection() will fail. In many use cases this may be appropriate, and if the caller checks the return result from SetJunctionDirection() then they can reattempt the call later. However, if it's critical that the junction is updated immediately, then script can instead request that the junction is loaded, as follows:

public thread void SetJunctionDirection(SecurityToken token, int direction)
{
  if (!m_junctionID)
    return;
  
  Junction junc = cast<Junction>(World.SynchronouslyLoadGameObjectByID(m_junctionID));
  if (!junc)
    return;     
  
  junc.SetDirection(token, direction);
}

There are several important things to note about this new function.

  1. The function will now force the tile/section which contains the junction object to be loaded into memory. If this junction set is gameplay critical, the forcing the tile to load is likely acceptable, but it does come with a performance cost and script should avoid doing this over multiple items.
  2. The function is now declared as a thread. Script threads allow an object to use function calls like wait() and Sleep() and in this case, to perform a synchronous object load. This simplifies the update somewhat, but to use the SynchronouslyLoadGameObjectByID() helper function the calling function must be running on a thread, so that it can wait for the asynchronous query to complete.
  3. The function no longer has a return code. This is necessary because the function is now asynchronous, meaning that the caller will continue execution after calling SetJunctionDirection(), before SetJunctionDirection() has a chance to run or complete. If it is necessary for the caller to be notified of the success or failure of this call, then this is best achieved with message posts.

It is worth noting that an object cannot have too many thread functions running on it at once, so too many calls to SetJunctionDirection() will result in a script exception with the error code ER_ThreadError.

Logging an ID

Consider the case where our Example class is being used in game, perhaps by a script rule, but doesn't seem to be working. It may be desirable to add diagnostic logging to the class in order to see where it is failing. With the old string name we could simply log the ID directly, as follows:

Interface.Log("Example.SetJunctionDirection> Junction is: " + m_junctionName);

This will not compile if we change m_junctionID to a GameObjectID, but a function does exist which allows us to get a log-able descriptive string from the ID. This debug string will also contain far more information about the ID being used, making it much more useful.

if (m_junctionID)
  Interface.Log("Example.SetJunctionDirection> Junction ID is: " + m_junctionID.GetDebugString());
else
  Interface.Log("Example.SetJunctionDirection> Junction ID is: (null)");

Don't forget that the new ID is an object type, and you must check it isn't null before attempting to get it's debug string. If you need to add a lot of logs like this you may want to consider adding yourself a helper function such as:

string GetGameObjectIDDebugString(GameObjectID obj)
{
  if (m_junctionID)
    return m_junctionID.GetDebugString();
  
  return "(null)";
}

Saving/Loading an ID

Often when a script is saving an item reference by name, it's because it intends to store it into properties Soup for a Session or savegame. For example, the script may be part of a Session Rule which sets changes a junction at some point in a Session. Let's suppose it has the following script to save and load the script name:

public Soup GetProperties(void)
{
  Soup data = Constructors.NewSoup();
  data.SetNamedTag("junction", m_junctionName);
  return data;
}

public void SetProperties(Soup soup)
{
  m_junctionName = data.GetNamedTag("junction", m_junctionName);
}

Updating these calls to use GameObjectID is straightforward, but we must make sure the reader supports both formats.

public Soup GetProperties(void)
{
  Soup data = Constructors.NewSoup();
  data.SetNamedTag("version", 1);
  data.SetNamedTag("junction-id", m_junctionID);
  return data;
}

public void SetProperties(Soup soup)
{
  int version = data.GetNamedTagAsInt("version", 0);
  if (version == 0)
  {
    // Legacy format, item is saved using script name.
    string junctionName = data.GetNamedTag("junction");
    
    if (junctionName == "")
    {
      // No junction configured, clear our saved ID.
      m_junctionID = null;
    }
    else
    {
      // See if the object is loaded, generating an error if not.
      Junction junc = cast<Junction>(Router.GetGameObject(junctionName));
      if (junc)
      {
        // Found it, upgrade to the new ID format.
        m_junctionID = junc.GetGameObjectID();
      }
      else
      {
        // Object not found. Generate an error, as this may break the Session.
        Exception("Example.SetProperties> Failed to load junction from legacy data: " + junctionName);
        m_junctionID = null;
      }
    }
  }
  else
  {
    m_junctionID = data.GetNamedTagAsGameObjectID("junction-id", m_junctionID);
  }
}

You'll notice that in order to support the legacy format we've had to add versioning information, and keep the obsolete function calls. In addition, if Route streaming is enabled then the junction may not be loaded when the function runs, so we've script to throw an exception in this case. This will make the error obvious to the player, so that they know straight away that if they continue to play the Session, it may not work.

Since we've kept the legacy function call to Router.GetGameObject(string), this script will generate compiler warning during validation. This is generally harmless, but may be undesirable in some cases. In order to suppress these warnings we can move our legacy format load into a separate function, and tag that function with the legacy_compatibility keyword.

legacy_compatibility void LoadJunctionFromLegacyName(string)
{
  if (junctionName == "")
  {
    // No junction configured, clear our saved ID.
    m_junctionID = null;
    return;
  }
  
  // See if the object is loaded, generating an error if not.
  Junction junc = cast<Junction>(Router.GetGameObject(junctionName));
  if (!junc)
  {
    // Object not found. Generate an error, as this may break the Session.
    Exception("Example.SetProperties> Failed to load junction from legacy data: " + junctionName);
    m_junctionID = null;
    return;
  }
  
  // Found it, upgrade to the new ID format.
  m_junctionID = junc.GetGameObjectID();
}

public void SetProperties(Soup soup)
{
  int version = data.GetNamedTagAsInt("version", 0);
  if (version == 0)
  {
    // Legacy format, item is saved using script name.
    LoadJunctionFromLegacyName(data.GetNamedTag("junction"));
  }
  else
  {
    m_junctionID = data.GetNamedTagAsGameObjectID("junction-id", m_junctionID);
  }
}

See Also

Personal tools