Ref Hacking

From Resonite Wiki
Revision as of 03:15, 8 June 2024 by Yosh (talk | contribs) (bikeshedding heading names and formatting styles)

Reference Hacking, commonly shortened to Ref Hacking, is a term to describe any method that dynamically utilizes Reference IDs to read from and write to world elements.

Common uses for ref hacking include accessing elements that exist outside of Root, accessing elements without knowing their reference ID beforehand (most commonly for dynamic component field access), and finding what components exist under a slot.

Ref hacking is not supported by the developers of Resonite. Depending on how one utilizes ref hacking in a creation, said creation may break across sessions, game updates, or may perhaps be resilient enough for a long while. Nothing is guaranteed, and it is important that this is understood before potential issues arise. If you find yourself using ref hacking consistently for a specific purpose, consider searching for, upvoting, or creating if it doesn't exist, a feature request on the Resonite issue tracker
Ref hacking, while alone is not against the User Guidelines, can be used to violate guidelines, like many other tools. Have common sense and question if you are ref hacking to bypass a lock to information that you shouldn't be accessing.

Basic Concepts

An Inspector with four components: PrimitiveMemberEditor, TextEditor, Text, and ReferenceField<IWorldElement>. For the PrimitiveMemberEditor, _target points to the Reference field on the ReferenceField, _textEditor to the TextEditor component, and _textDrive to the Content field on the Text component.
The four core components to ref hacking. The ID of the referenced element (in this case, a permission role name) is contained in the Content field of the Text component.

Before jumping into ref hacking, it's important to internalize a few concepts on how reference IDs are allocated when ref hacking on existing elements:

  • Reference IDs for slots are allocated in a depth-first search manner and in the order as they appear in the inspector.
  • Reference IDs for components of a slot reside between the slot the component is under and the next slot, and are allocated in the same order as they appear in the inspector (do note, however, that the order of components themselves is *not* guaranteed, just that the two follow the same order).
  • Reference IDs for component fields always reside between two components, but are not guaranteed to be allocated in the same order as they appear in the inspector.
  • The "reference ID offset" between a world element and a particular field in the element changes across sessions, but is consistent within any given session. E.g. the ID offset between any given Text component and its Content field is the same for all Text components in the session.

All points but the fourth do not apply for newly created elements, such as new slots on an object, new components on a slot, or new fields of a component (i.e. elements in a list). In this case, simply the next available reference ID is used for the new element, and it is not guaranteed to follow the first three rules above.

With that out of the way, the core of ref hacking involves a PrimitiveMemberEditor, ReferenceField<IWorldElement>, Text component, and TextEditor. The Reference field of the ReferenceField is of type SyncRef, which has a RefID as its Value. The RefID type contains a member, id, which is the reference ID of the referenced element. This ID is then accessed via the PrimitiveMemberEditor by setting _path to id, which implicitly converts the ID to an integer string to drive the Content field of the Text component.

From there, one can cast the string to a ulong to perform math on it. By writing a number string back to the Content field of the text component, the reference in the ReferenceField will be updated to the element that corresponds to that RefID. This essentially acts as a way to "dereference" a reference ID as the element it points to.

Finding Elements

There are several ways to build upon the basic concept outlined above to find elements dynamically, and nearly all of them rely on two ways to work with reference IDs: offsets and iteration.

The inspector from before with two ValueFields on it as well. A ProtoFlux chain of two references--the first ValueField and its Value--each going into a RefID, string remove the first 2 characters, then parsing each as a ulong and subtracting the field from the component is shown. This gets the offset of the field and component and stores it in a ulong data model store on oad. This is then used to add and cast back the 2nd ValueField's Value without having a direct reference to the Value field on the ValueField.
A simple offset calculator setup. The RefID difference between the first ValueField and its Value is used for the second ValueField and its Value, resulting in the 2nd ValueField's Sync<int> being returned as an IWorldElement. This offset will be the same for all ValueFields in a session. For simplicity, the 2nd ValueField reference is used directly, but in reality one would most likely need to get this via ref hacking as well.

Offsets

Offsets are the easier and more performant way to work with reference IDs, but can only be used effectively with enough information and are less flexible. As such, offsets are usually used for dynamically accessing component fields.

One might naïvely find the offset between a known element and the field one wants to access, then hard-code the values into code to use later. However, recall point 4 from above: this can and will break across different sessions. To combat this, one can make an offset calculator. Essentially, by creating a static, known clone of what will be dynamically accessed (i.e. a static slot using the same single component) and calculating the offset between the parent element and the intended child element, one can use the same offset for dynamic instances of the same setup. This will work across different sessions, since the offset between a particular parent element and a child of the element within the same setup is consistent in any given session.

Iteration

Iteration is the more expensive, yet more flexible way to work with reference IDs. The basic routine involves finding a "base" ID to start at, then repeatedly adding 256 to the current reference ID until a certain condition is met. Common conditions for stopping a reference iteration inlcude type checking, cast checking, dereference checking, and name checking.

Type Checking

Type checking utilizes the Get Type node to get the type of the referenced IWorldElement, then seeing if it's equal to a needed type. This method excels if there is only one field or component of a particular type under the parent element being iterated over.

Cast Checking

Cast checking involves creating an Object Cast with an input type of IWorldElement and an output type of the desired element, then plugging that in to an Is Null node. The loop will iterate until it reaches a type that can be casted into the desired type. This has the added caveat that any type that can be casted into the output type will stop the loop, not just the exact same type. However, this is a highly unlikely scenario to encounter with this, and it also allows one to access the object directly in case one needs to manipulate it with other nodes.

Dereference Checking

Dereference checking is very similar to cast checking, but is used when one also needs access to the referenced object in a SyncRef field. This uses the Reference Target node, where the target type is that of which the SyncRef points to.

Name Checking

Name checking utilizes the RefEditor component and parsing the name of the elements being iterated over until it matches the desired name. To use it, a RefEditor somewhere (usually on the same slot as the PrimitiveMemberEditor) should have its _targetRef set to the Reference field on the ReferenceField<IWorldElement> and the _textDrive set to the Content a ValueField<string>. This allows one to read the name of whatever the ReferenceField<IWorldElement> is pointing to.

Name checking is one of the most flexible ways to check for a stopping point, but comes with its own caveats. For one, the parsing code must be written robustly, and improper parsing code can cause disaster such as infinite loops. Additionally, it takes 1 or 2 updates for a RefEditor to update the contents of its _textDrive, so one must use an ASync While node with an Delay Updates node for each iteration, which makes using name checking not instant, unlike the other methods.

BagEditor

The BagEditor component is a special component with the niche use case of being able to iterate over and find elements of an ISyncBag, such as the UserBag of users in a session and WorkerBag of components on a slot. Upon filling the _targetBag field, the children of the slot will be populated with the members of the targeted bag. Each child will have its RefID in the slot name to parse out.

Changing the _targetBag field does not do anything once it has been filled. To change the bag being pointed to, one must clear the _targetBag field, duplicate the slot containing the BagEditor, then refhack to fill the _targetBag field again.

Accessing Elements

Reading

Reading the value of an IWorldElelement depends on whether the element is an Object or a primitive. For objects, a simple Object Cast from IWorldElement to the intended type is sufficient, just like what one does when cast checking.

Primitives are trickier to read, since they are not objects, but rather get wrapped in a Sync<T>. However, by using a ValueDriver component and writing the Sync<T> to it, and sourcing the field that DriveTarget points to, it is possible to directly access the primitive value. It is also possible to use To String on the Sync<T> and parse a primitive using the Parse node.

Writing

Writing to the referenced IWorldElement is done via the Field As Variable node and the Indirect Write node. Directly casting an IWorldElement to a IVariable does not work; it has to go through the Field As Variable node.

Driving

Driving the referenced IWorldElement is done via the Field Hook node. One must first create an ObjectCast from IWorldElement to IField<T>, where T is the type of the field.