The Interface System in OSRS handles the rendering and logic for all GUI components shown to the player by the OSRS Game Client. Whilst most of this logic happens client-side, the Server is often aware when certain interfaces are open, to validate incoming events that the user may have initiated. Interfaces can be built using ClientScript, and have script hooks to allow scripts to run when certain events happen (e.g. on-mouse-over, on-key-down).
Each interface element, called a 'component', has a type that determines its basic appearance and behaviour. Further behaviour can be added to components using script hooks (detailed further down).
All components have the following properties:
Below is a table of component types and the properties associated with each:
| ID | Type | Description | Properties |
|---|---|---|---|
| 0 | Layer | Parent container for other components. Child components are positioned relative to their parent layer. | Scroll X/Y/Width/Height |
| 3 | Rect | Rectangle that can be filled, outlined, etc. | Colour / Fill / Transparency / Colour 2 (for gradients) / Transparency 2 (for gradients) |
| 4 | Text | Text Label | Colour / Font / Text / Line spacing / Alignment (horizontal & vertical) / Shadow |
| 5 | Graphic | Shows a sprite | Graphic / Graphic 2 / HTTP-graphic-URL / Rotate / Tiling / Outline / Shadow colour / V-Flip / H-Flip |
| 6 | Model | Shows a model that can be positioned and angled | Model / Anim / X/Y offset / X/Y angle / Zoom / Width/Height / Orthogonal / Game overlay / ObjFlags |
| 9 | Line | Shows a straight line | Colour / Stroke width |
| 10 | Circle | Shows a circle, or arc of a circle, that can be filled, outlined, etc. | Colour / Fill / Transparency / Stroke width / Start angle (zero is up, 0-65535) / Stop angle |
| 12 | InputField | A text input field with support for caret, selection, copy, paste, filters, etc. More details here: InputField (Component type) | Colour / Shadow / Several additional properties |
Static components are defined at design-time and unpacked from the game's js5 files at runtime. When you retrieve an IfGroup, the components field contains the array of all static components for that interface.
Dynamic components are created at runtime via runescript. Until very recently, dynamic components could only be added to static LAYER components. But in the latest engine, dynamic components can now be added to dynamic layers, allowing for deep hierarchies of UI elements.
IfType is the monolithic class that handles properties for all components. Each component is an instance of IfType, and has fields for all properties regardless of what type it represents. This excludes CRMView and InputFields, as they are complex components that have a wrapper for their properties and behaviours.
IfType also contains all script hooks for each component, and runtime data such as whether this component was clicked or mouse-over-ed. Lua plugins are able to register for all the same script hooks as runescript can.
IfGroups are a concept that only exists in the Plugin API. At the engine level there is no top-level object that represents a discrete UI system like the chatbox; rather there is an array of static components that all render together, via a call to a method called "DrawInterface". This method also takes in screen offsets, which are now cached and exposed via the coordinates field of IFGroup.
"If I fiddle with the position of any IfTypes in a group, I see the change briefly take effect, but then it snaps back to default! How do I make a persistent change to an IfType?"
Game scripts and server messages will regularly update game interfaces back to their regular, canonical values. This makes persistent changes challenging. A potential workaround is to use the ON_IF_COMPONENT_CHANGED event. This will fire on any visual-affecting update to an IfType component. You could use a pattern like:
function persistChangeToInterface(ifType)
local applyChange = function(ifType)
ifType.children:get(1).dataX = 32
end
--apply initial changes.
applyChange(ifType)
--make sure changes are re-applied every time the IfType changes.
ifType:subscribe(
function(hook)
applyChange(hook.eventComponent)
end,
osrs.Events.ON_IF_COMPONENT_CHANGED)
end
This event fires often, and might not be suitable for very in-depth, complicated changes.
"It looks like I could register for all the same events via IfType:subscribe that I can register via osrs.Events.subscribe, but is there any reason to do so?"
There is no particular benefit to registering for general events like ON_STAT_TRANSMIT on a specific IfType. It might be convenient if you want to update an IfType in response to an event transmission, since the returned HookReq contains a reference to the IfType you registered on. However, generally, it will be harder to decode information from a HookReq than from one of the dedicated event handlers.
"I want to register for an event that fires when a whole IfGroup (like, say inventory), starts and stops rendering. How do I do that?
Unfortunately the engine doesn't expose a convenient event hook for this concept. You will have to poll the isRendering property. Depending on the UI you might need to be selective in picking which component of the group to poll; frequently, just selecting the first LAYER component in the static component list will be sufficient.
Example:
-- monitors rendering status of a component, polling every 100ms.
function monitorComponent(comp, callback)
local lastRenderState = comp.isRendering
osrs.setInterval(function()
if comp.isRendering ~= lastRenderState then
callback(comp.isRendering, lastRenderState)
lastRenderState = comp.isRendering
end
return true
end, 0.100)
"I want to unsubscribe an IfType event callback from within the body of the event callback itself--how do I do this?"
There is a lua subtlety to keep in mind here. If you write code like this:
-- This doesn't work!
local cb = function() comp:unsubscribe(cb, eventId) end
The variable cb being closed over is a global variable with value nil.
Instead, pre-declare your local variable. That way a reference to it will be properly captured in the function body:
-- instead do this:
local cb
cb = function() comp:unsubscribe(cb, eventId) end