Ref Hacking: Difference between revisions

From Resonite Wiki
Added links.
fill out a whoolllle lot of info, TODO: add more images, but here's everything in text form :D
Line 1: Line 1:
'''Reference Hacking''', commonly shortened to '''Ref Hacking''', is a term to describe any method that dynamically utilizes [[Type:RefID|Reference IDs]] to read from and write to [[Type:IWorldElement|world elements]].
'''Reference Hacking''', commonly shortened to '''Ref Hacking''', is a term to describe any method that dynamically utilizes [[Type:RefID|Reference IDs]] to read from and write to [[Type:IWorldElement|world elements]].


Common uses for ref hacking include accessing elements that exist outside of [[Root]], accessing elements without knowing their reference ID beforehand, 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 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, an issue on [https://github.com/Yellow-Dog-Man/Resonite-Issues/issues the Resonite issue tracker]|danger}}
{{Note|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 [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}}
Line 20: Line 20:
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.
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 [[Component:PrimitiveMemberEditor|PrimitiveMemberEditor]], [[Component:ReferenceField|ReferenceField<IWorldElement>]], [[Component:Text|Text component]], and [[Component:TextEditor|TextEditor]]. The <code>Reference</code> field of the ReferenceField is of type [[Type:SyncRef`1|SyncRef]], and the SyncRef type contains a "hidden" member, <code>id</code>, which is the reference ID of the referenced element. This ID is then accessed via the PrimitiveMemberEditor, which implicitly converts the ID to a string to drive the <code>Content</code> field of the Text component. From there, one can cast the string to a [[Type:ULong|ulong]] to perform math on it.
With that out of the way, the core of ref hacking involves a [[Component:PrimitiveMemberEditor|PrimitiveMemberEditor]], [[Component:ReferenceField|ReferenceField<IWorldElement>]], [[Component:Text|Text component]], and [[Component:TextEditor|TextEditor]]. The <code>Reference</code> field of the ReferenceField is of type [[Type:SyncRef`1|SyncRef]], which has a [[Type:RefID|RefID]] as its <code>Value</code>. The RefID type contains a member, <code>id</code>, which is the reference ID of the referenced element. This ID is then accessed via the PrimitiveMemberEditor by setting <code>_path</code> to <code>id</code>, which implicitly converts the ID to an integer string to drive the <code>Content</code> field of the Text component.


== Taking It Further ==
From there, one can cast the string to a [[Type:ULong|ulong]] to perform math on it. By writing a number string back to the <code>Content</code> 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.


There are several ways to build upon the basic concept outlined above, and nearly all of them rely on two ways to work with reference IDs: '''offsets''' and '''iteration'''.
== 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'''.
 
[[File:Ref hacking offset calculator.webp|thumb|left|alt=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 [[Component:ValueField|ValueField]] and its <code>Value</code> is used for the second ValueField and its <code>Value</code>, resulting in the 2nd ValueField's [[Type:Sync|Sync]]<int> being returned as an [[Type:IWorldElement|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 ===
Line 30: Line 34:
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.
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 the setup you are searching for (i.e. using the same component) and calculating the offset between the parent element and the intended element, then using the same offset for dynamic instances, this will work across different sessions, since the offset between a particular parent element and a field of the element is consistent in any given session.
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 ===


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 <code>256</code> to the current reference ID until a certain condition is met.
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 <code>256</code> to the current reference ID until a certain condition is met. Common conditions for stopping a reference iteration involve '''type checking''', '''cast checking''', '''dereference checking''', and '''name checking'''.
 
'''Type checking''' utilizes the [[ProtoFlux:Get Type|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''' involves creating an [[ProtoFlux:Object Cast|Object Cast]] with an input type of IWorldElement and an output type of the desired element, then plugging that in to an [[ProtoFlux:Is Null|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''' is very similar to cast checking, but is used when one also needs access to the referenced object in a [[Type:SyncRef|SyncRef]] field. This uses the [[ProtoFlux:Reference Target|Reference Target]] node, where the target type is that of which the SyncRef points to.
 
'''Name checking''' utilizes the [[Component:RefEditor|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 <code>_targetRef</code> set to the <code>Reference</code>field on the ReferenceField<IWorldElement> and the <code>_textDrive</code> set to the <code>Content</code> a [[Component:ValueField|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 <code>_textDrive</code>, so one must use an [[ProtoFlux:ASync While|ASync While]] node with an [[ProtoFlux:Delay Updates|Delay Updates]] node for each iteration, which makes using name checking not instant, unlike the other methods.
 
=== BagEditor ===
 
The [[Component:BagEditor|BagEditor]] component is a special component with the niche use case of being able to iterate over and find elements of an [[Type:ISyncBag|ISyncBag]], such as the UserBag of users in a session and WorkerBag of components on a slot.
 
== Accessing Elements ==
 
=== Reading ===
 
Reading the value of an IWorldElelement depends on whether the element is an [[Type:Object|Object]] or a primitive. For objects, a simple [[ProtoFlux:Object Cast|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 [[Type:Sync|Sync]]<T>. However, by using a [[Component:ValueDriver|ValueDriver]] component and writing the Sync<T> to it, and sourcing the field that <code>DriveTarget</code> point to, it is possible to directly access the primitive value. It is also possible to use [[ProtoFlux:To String|To String]] on the Sync<T> and parse a primitive using the [[ProtoFlux:Parse|Parse]] node.
 
=== Writing ===
 
Writing to the referenced IWorldElement is done via the [[ProtoFlux:Field As Variable|Field As Variable]] node and the [[ProtoFlux:Indirect Write|Indirect Write]] node. Directly casting an IWorldElement to a [[Type:IVariable|IVariable]] does not work; it has to go through the Field As Variable node.
 
=== Driving ===
 
Driving the referenced IWorldElement is done via the [[ProtoFlux:Field Hook|Field Hook]] node. One must first create an ObjectCast from IWorldElement to IField<T>, where T is the type of the field.

Revision as of 22:25, 5 June 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.

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 involve type checking, cast checking, dereference checking, and name 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 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 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 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 Referencefield 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.

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 point 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.