HowTo/Upgrade GameObject.GetId()

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

Jump to: navigation, search

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

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


Contents

Overview

The script function GameObject.GetId() returns an integer ID which identifies an object within the script context. This ID is unique and constant for the life of the object only. When the specific object reference is released the ID becomes usable by other objects, and if the game is reloaded the world item represented by that GameObject (e.g. a Signal) will have some other ID.

This makes this ID of limited usefulness, and with the introduction of Route streaming they cannot be trusted and are considered obsolete. The modern replacement function is

public native GameObjectID GameObject.GetGameObjectID(void);

Getting an Object ID

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

class Example
{
  int             m_gameObjectID = -1;
  
  public void SetObject(GameObject obj)
  {
    // Save the objects ID.
    if (obj)
      m_gameObjectID = obj.GetId();
    else
      m_gameObjectID = -1;
  }
};

This class has a single member variable used to store the ID of an object, and a function which sets the object reference. To upgrade the script to support modern versions of Trainz, the integer ID must be replaced with a GameObjectID, and the GetId() 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
{
  int             m_junctionID = -1;
  
  public void SetJunction(Junction junc)
  {
    // Save the junctions ID.
    if (junc)
      m_junctionID = junc.GetId();
    else
      m_junctionID = -1;
  }
  
  public bool SetJunctionDirection(SecurityToken token, int direction)
  {
    if (m_junctionID < 0)
      return false;
    
    Junction junc = cast<Junction>(Router.GetGameObject(m_junctionID));
    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 it's integer ID can change. 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 integer IDs, we could simply log the ID directly, as follows:

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

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

It is not valid to save an integer ID into long term storage such as a properties Soup, as the ID will change between runs of the game, but there are some scripts out there which attempt it. Such scripts should be considered broken, and in need of repair, and updating them to use GameObjectID is the ideal way to repair them.

Lets assume our Example class is such a faulty script, and contains the following property saving calls:

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

public void SetProperties(Soup soup)
{
  m_junctionID = data.GetNamedTagAsInt("junction-id", m_junctionID);
}

Updating these calls to use GameObjectID is straightforward, and since we know the old approach was flawed we explicitly do not want to support reading it back in.

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)
  {
    int brokenID = data.GetNamedTagAsInt("junction-id", -1);
    if (brokenID >= 0)
    {
      // Old format (broken) data. This approach will not work, throw an error.
      Exception("Example.SetProperties> ERROR: Broken ID data detected)");
    }
    
    m_junctionID = null;
  }
  else
  {
    m_junctionID = data.GetNamedTagAsGameObjectID("junction-id", m_junctionID);
  }
}


See Also

Personal tools