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.
Basic Concepts
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.
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.
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.