change reading to use the Canonical way, that being the source node |
m Fix missing text from previous update |
||
Line 3: | Line 3: | ||
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. | 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. | ||
{{Note|Ref hacking is not supported by the [[Resonite Team|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 | {{Note|Ref hacking is not supported by the [[Resonite Team|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 [https://github.com/Yellow-Dog-Man/Resonite-Issues/issues the Resonite issue tracker]|danger}} | ||
{{Note|Ref hacking, while alone is not against the [https://resonite.com/policies/Guidelines.html 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.|warning}} | {{Note|Ref hacking, while alone is not against the [https://resonite.com/policies/Guidelines.html 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.|warning}} |
Latest revision as of 11:18, 16 December 2024
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.
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
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.
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.
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.
Accessing Elements
Reading
Reading the value of an IWorldElelement depends on whether the element is a reference type or a value type. For references, a simple Object Cast from IWorldElement to the intended type is sufficient, just like what one does when cast checking.
Values are trickier to read, since they are not direct representations of the value, but rather get wrapped in a reference type, such as Sync<T>. However, it's not too terribly difficult. Every wrapper around a value type implements IValue<T>, meaning that one can use a Source node to access the underlying value by writing the reference type to the GlobalReference<IValue<T>>.
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.