HSL Oracle Systems
- This page is about the HSL-created system for managing game definitions known as Spec Oracles. For the database application, see Category:Oracle Database.
The Oracle Pattern
The game client knows almost nothing about server data and even less about data created and consumed by serverside HSL systems. This limitation is at war with the need for HSL systems ( in particular GUIs ) to display information to the player. The Oracle Pattern describes a way of implementing our systems to provide clientside access to data that is held on the server.
This pattern is named the "Oracle Pattern" because it describes a method of structuring data for storage on the client to mirror server data, without requiring that the server re-transmit data. In essence, it is the creation of an in-memory datastore that the client can query for information that it is supposed to know about. It "knows" things by being fed information from the server in some manner, and caches this knowledge for future requests.
Oracles provide interfaces for working with and listening to the data models they support.
What problem does the Oracle Pattern solve?
- providing convenient ( for scripters ) access to server data on the client
- exposing data known to the server in a safe manner to the client
- minimizing bandwidth usage by facilitating reuse of data
- (optionally) support asynchronous access to data
What problems does the Oracle Pattern not solve?
- handling data that is unique and/or extremely transitory in nature
Oracles are Nodes
On the client, an oracle is represented by a node ( noderef of class SomeClass ). The class from which the oracle node is instantiated in turn commonly inherits from the collection classes ( most often CollectionUnorderedSet ) so that it is able to add/remove/list the data nodes it stores/caches.
Data stored by Oracles are nodes
Oracles store their data as nodes.
Oracles expose an interface to work with their data
Oracles provide a means of working with their nodes through the functions they expose.
Oracles have ClientSide Classes and may have ServerSide Classes that support them
An oracle is manifested as a node instantiated from the specific oracle class, the class methods script for that class then exposes methods to work with the oracle's data. Some oracles will simply ask the server the first time data is required, other oracles may have complex serverside classes supporting their functionality. These serverside expressions of oracles handle the work required to make the data accessible to the client oracle, through Inter-Process Communication.
Implementation of the Oracle Pattern is typically used to minimize the bandwidth costs and increase the responsiveness of the GUI on the client. By caching a local copy of serverside data, and updating it with changes as is appropriate, there is no need to send repeated transmissions to the server requesting data for display in a GUI. Additionally, by exposing the cached data to general queries, other GUIs can reuse the data, reducing or eliminating the need for them to directly ask the server for data.
Consumers of the data may either make direct requests/queries through the Oracle's API or may listen for updates to the data using an implementation of the Observer Pattern. Consistent implementation of this pattern will eventually enable players to create their own GUIs as consumers of one or more Oracles' data, to display the information in a way that is pleasing to them.
Let's say you are tasked to design an inventory system including both the serverside and clientside representations of an inventory. For this particular implementation ,the server-side oracle is represented by the character's Inventory node ( and hierarchy of items therein ). For the client-side representation of Inventory it turns out to be convenient to mimic the structure on the server, so we can take advantage of the built-in transmission capabilities of HSL collections. Once a copy of Inventory is received on the client it needs to be anchored to some well-known node so it can be found by all consumers of its data.
- Data must be exposed in a safe manner to potential player scripting
- Data in an oracle should be accessed only through a single interface. (i.e. GUIs using the data should be asking the oracle script, not accessing data on the data structure itself.)
- Exposed functionality must be documented in a clear manner so that it will be easy for a player to know what to expect.
- Updates to data should send notification via the client event system or an implementation of the Observer Pattern when possible so that GUIs can listen for changes rather than require constant polling of an oracle.
- Data should only be sent to an oracle if the player client really is supposed to know about it. ( do not send information the client is not supposed to know )
- The server should update oracles, which in turn update GUIs. The server should not make calls to update a GUI directly.
Populate initial data set
Client oracle's data set needs to be initialized with the current server oracle's data set at the start of a session, thereafter updates should be sufficent to synchronize the client view of the data to that of the server. Which leads to the question, how do we populate the initial data set for the client? Most commonly during the login process the server's oracle node or a procedure in a system script is notified that the client needs a copy of the data. The data is then packaged in a format the client understands and is sent to the client in the args field of a remotecall. Upon receipt by the client, the data is unpacked into one or more nodes which are then anchored to a well known interface.
Optionally, the first time a client oracle is asked for data it does not yet have it may make a request for the server to send a copy of the current data set. Any subsequent requests are put into a queue pending the arrival of the initial data set.
Associate data to a Root Node
The local data cache (represented by 1...n nodes) must to be associated in some known fashion to facilitate the use of that data by its consumers. The root node on the client from which you may anchor client data is the world anchor, on the server the root node is either the arearoot, the account node, or the character node. For data that is account specific, the account node should be used for the anchor. For data that is character specific, the character node should be used as the anchor.
Typically, a system node is created with methods for querying and subscribing to the system. The system node is anchored to one of the root nodes, and all data nodes for the system are in turn anchored to the system node.
Get the area root
// Server side root node for areas var areaRoot = GetRootNode()
Get the world anchor
// Client side root node var wa = SYSTEM.INFO.WORLDANCHOR
Data is so commonly anchored to the world anchor on the client that a utility script was created specifically to deal with anchoring system data in using a set of common utilities, this script is the ClientDataSystem.
Required Utility: ClientDataSystem
The ClientDataSystem script provides a way of anchoring node hierarchies of data in a well known way on the client. Oracles will commonly take advantage of the ClientDataSystem script to handle the anchoring of a system root node in a manner that it can be easily found. Oracles may optionally implement methods in the World_AnchorClassMethods script enabling easy access to a system root node using method calls on the world_anchor node which is eaisly located using the system variable SYSTEM.INFO.WORLDANCHOR
Get the Anchor for your system
// This will get and/or create a node in the clientDataSystem hierarchy to which you may // anchor the nodes for your system. ClientDataSystem:GetSystemRoot( "YourSystemName" )
Add a node/hierarchy to your system node
// Anchor a node to your sytem root, since it is a node you in effect // may anchor a node hierarchy by adding the top level node to your system ClientDataSystem:AddDataToSystem( "YourSystemName", yourNode )
Get a list of nodes in your system
yourData as list of id = ClientDataSystem:GetDataForSystem( "YourSystemName" )
Send notifications as server data changes
Once you have the initial data set on the client, keeping it synchronized with the server requires the notifications be sent to the client when the data changes.
There are several ways you can implement the notification mechanic;
- Set a watching script on the fields which are to be reflected to the client, and send a remote call when a field changes.
- Use object oriented programming techniques like a setter/getter pattern on private field in the class. The setter handles sending notification events
- Create utilities that manipulate the data procedurally and only change the data using those procedures which handle notification events
- Set up periodic polling of the data and push updates when a hasChanged flag has been set
No matter which mechanic you choose to identify that the server data has changed, in the end it all boils down to using Inter-Process Communication to send serialized data to the client oracles to update their local caches.
// Notify the client inventory that an item has been destroyed rout as Class RemoteCallClientOut rout.toPlayer = inventoryPossessor.GetMyAccount() rout.toScript = "InventoryClassMethods" rout.toFunction = "RemoteDestroyInventoryItem" rout.failScript = debugUtils rout.failFunction = "ClientRMCFail" rout.args["item"] = item RemoteCallClient(rout)