Ref Hacking

From Resonite Wiki

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.

Fundamental Concepts

Before jumping into ref hacking, it's valuable to internalize a few concepts on the backend side to aid work one does with it.

RefIDs and Allocation

Every IWorldElement that exists in a world has a RefID. RefIDs are generally allocated on a "whatever is available" basis. However, when spawning out items, there are certain patterns that one can take advantage of. For all of the following points, an element being allocated "after" another means that its reference ID is greater by 256.

  • Reference IDs for slots and their children 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 last byte of a reference ID represents the allocating user of the element. Consequently, reference IDs are generally aligned to one particular value modulo 256 for any particular item allocated by a user. If a different user adds elements to an object, it will be misaligned with the other elements until the object is reallocated in full, usually via saving to inventory and spawning out again.
  • The "reference ID offset" between a world element and a particular child element in the same setup 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 "child elements", such as new slots on a parent slot, new components on a slot, or new dynamic 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.

PrimitiveMemberEditor and ReferenceField

The PrimitiveMemberEditor component is intended to be used for UIX elements to edit fields of a struct, such as the x component of a float3. The internals of how it does this is much too convoluted for this wiki page, but a specific setup allows for it to access reference IDs.

The Reference field of a ReferenceField<IWorldElement> component is of type SyncRef<IWorldElement>, and this type internally has a Value of type RefID, which corresponds to the element that the field references. The PrimitiveMemberEditor, then, when _target is set to the Reference field, resolves the "base" type to be a RefID and thus uses it as the jumping off point. The RefID struct contains a member, id, which is simply the Ulong representation of the RefID. This allows one to directly access and modify a reference ID number and source what the number references to.

Core Setup

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.

With that out of the way, to make a ref hacking setup, one should place a PrimitiveMemberEditor, ReferenceField<IWorldElement>, Text component, and TextEditor on a slot together. For the PrimitiveMemberEditor, set _path to the string id, set _target to the Reference field of the ReferenceField<IWorldElement>, set _textEditor to the TextEditor component, and set _textDrive to the Content field of the Text component. Ensure that the Text field of the TextEditor points to the Text component on the slot as well, else the setup will break.

One should notice that the Content field will now display a number. From there, casting the string to a ulong allows math to be performed on it. By writing a number string back to the Content field, the Reference in the ReferenceField will be updated to the element that corresponds to the RefID with that numeric value. 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. 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 fields on a component, but can nonetheless be used for components on a slot if one knows the expected setup beforehand.

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 a key fact about RefID allocation: 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.

Getting the Uri field of a StaticAudioClip using iteration. The iteration starts at the slot containing the clip. Three separate methods are presented, from top to bottom: name checking, type checking, and cast checking. Note how the name check requires an updates delay node.

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 update 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 or Reference As Variable nodes 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.