Page 1 of 1

Are sub-OXPs possible?

Posted: Fri Oct 12, 2007 5:16 am
by Cmdr. Maegil
Is it possible for an OXP to read data from another?

More specifically, can wr make the Hoopy OXP search for others (e.g.: Lounge*.OXP), choose one of the available, print text from it on the mission screen and then start the second OXP's script?


This came to me while muling over the pilot's lounge screen: even if Ahruman won't fit one to the game itself, it could still be made on the Hoopys.
Beguinning by adding an option to go to the lounge to the gamble yes/no screen, do a brief description of the ambient and some of the patrons, -adding one from the mission OXP- and putting a sequence of choices that will trigger the mission (or not).

What do you think?

Posted: Fri Oct 12, 2007 9:11 am
by JensAyton
OXPs can use each other’s resources. This has happened a few times accidentally. It’s also why it’s so important to make an effort to have unique names. :-)

There’s no way for legacy scripts to communicate with each other. However, I intend to make it possible to look for world scripts in JavaScript. Something like this:

Code: Select all

let hoopyScript = worldScripts["name-of-hoopy-script"]
if (hoopyScript)
{
    // Do stuff relying on/interacting with hoopy script
}
This tells you whether the OXP with a script called “name-of-hoopy-script” is installed. It also lets you call methods on that script or set properties of that script.

Rather than having HOopy search for other scripts at startup, I’d have scripts which offer services/extensions to HOopy register themselves with HOopy at startup time. Conveniently, the startUp() event handler is called once at startup time, after all world scripts have been loaded. I’ve cooked up an example, but can’t test it yet because I have to implement the worldScripts object in Oolite first. (It was already on my to-do list.) Oh, and I’ve got to do some other stuff first.

Posted: Fri Oct 12, 2007 10:04 am
by LittleBear
You could do it (in the current version of Oolite) by giving the piolts lounge screen when the player is docked at the HoOpy and the screen is status. This would mean the player only see the Lounge screen if docked at a HoOpy and had either said no to gambling or had finished gambling. Eg:-

Code: Select all

{ 
conditions = ("status_string equal STATUS_DOCKED", "dockedStationName_string equal CoachWhip hOopy Casino", "mission_hoopy_casino equal HOOPY_ENTER", missionChoice_string equal NO" "gui_screen_string equal GUI_SCREEN_STATUS); 
do = (your stuff here...
Murgh uses this key to give the No option:-

Code: Select all

<key>NO</key>
<string>No games of hazard for me.</string>
[code]

If you put the same key in your OXP, but changed it to:-
[code]
<key>NO</key>
<string>No games of hazard for me, I'd rather check out the piolts lounge</string>
Then your text would replace the No option in HoOpy and the player would know that by selecting No he goes to the piolts loung.

Your OXP would never do anything if the player didn't have HoOpy installed as the player could never meet the conditions. But if the player did and was docked at a HoOpy then your script with choices would be executed as soon as the Status screen came up and the player had chosen not to gamble.[/code]

Posted: Fri Oct 12, 2007 1:11 pm
by JensAyton
LittleBear: yes, you could do that, but it’s rather inflexible. For instance, you couldn’t have more than one script extending HOopy that way. Cmdr. Maegil’s question suggested, to me, a need for greater flexibility.

Anyway, here’s a pair of scripts (currently only usable on my computer, but hey) which implement a client-server model. Any number of “client” scripts can register with the “server”, which can then call the client scripts to, well, do whatever needs to be done. In this case, to select mission screen contents and choices. (Actually, the ability to do this is somewhat limited since choices can still only be set by a missiontext.plist key. Hmm.)

This is somewhat more programmerese-intensive JavaScript than you’re likely to need for typical mission script ports. There are ostensibly similar, but clunkier, ways of achieving the same thing. Fortunately, it’s copy-and-pasteable; just copy-and-paste everything except the last two testing methods and *poof*!, your script is a server.

Incidentally, this is quite similar to the technique I outlined earlier for the racing OXP, except there a ship would be the server.

Server script template:

Code: Select all

this.name = "ahruman-test-server"
this.version = "1.0"

this.scriptRegistry = []  // empty array


// Add a script to the registry. Each client script should register itself this way.
this.registerScript = function(script)
{
    if (script)
    {
        // Add script to scriptRegistry
        this.scriptRegistry.push(script)
    }
}


// Returns an array of the registered scripts.
this.registeredScripts = function()
{
    return this.scriptRegistry
}


/*  Calls the method "methodName" on each script, with arbitrary arguments.
    Example: serverScript.callScripts("doSomething", 1, 3)
             Calls doSomething(1, 3) on each registered script
             which has a doSomething() method.
*/
this.callScripts = function(methodName)
{
    /*  Create an array variable based on the special variable "arguments",
        which contains all the arguments (parameters) of the current function.
        arguments looks like an Array, but it isn't. In particular, it doesn't
        have the slice() method we'd otherwise use to get all but the first
        element. (We want to remove the first element because that's the same
        as methodNames, and we don't want to pass that to the script method.
    */
    let methodArgs = []
    for (let i = 1; i < arguments.length; i++)
    {
        methodArgs.push(arguments[i])
    }
    
    // Function which will be called for each script by forEach
    function callScriptOn(script)
    {
        // If the script has a property called methodName...
        if (script[methodName] != undefined)
        {
            let method = script[methodName]
            // ...and the property is actually a method...
            if (typeof method == "function")
            {
                // ...call method with object as "this" and the arguments.
                method.apply(script, methodArgs)
            }
        }
    }
    
    // Apply callScriptOn() to all elements of scriptRegistry.
    scriptRegistry.forEach(callScriptOn, this)
}


//  For testing purposes, call script methods each time the player launches or docks.
this.didDock = function()
{
    this.callScripts("testDockCallback", 42)
}


this.didLaunch = function()
{
    this.callScripts("testLaunchCallback", "banana")
}
Example client script

Code: Select all

this.name = "ahruman-test-client"
this.version = "1.0"


this.startUp = function()
{
    if (worldScripts["ahruman-test-server"] != undefined)
    {
        // Server script is installed, register.
        let server = worldScripts["ahruman-test-server"]
        
        this.setUpClientMethods()
        server.registerScript(this)
    }
    
    // Conserve memory by not keeping functions around that won't be called again.
    delete this.startUp
    delete this.setUpClientMethods
}


this.setUpClientMethods = function()
{
    // Set up the methods the server will call.
    this.testDockCallback = function(number)
    {
        LogWithClass("clientServerTest.dock", "The server tells me the player docked at docking slot " + number + ".")
    }
    
    this.testLaunchCallback = function(fruit)
    {
        LogWithClass("clientServerTest.launch", "The server tells me the player launched. The in-flight snack will be " + fruit + ".")
    }
}

Posted: Fri Oct 12, 2007 1:29 pm
by Eric Walch
LB's advise to change

Code: Select all

 
<key>NO</key> 
<string>No games of hazard for me.</string> 

into

Code: Select all

 
<key>NO</key> 
<string>No games of hazard for me, I'd rather check out the piolts lounge</string> 

Will not work. Both OXP's could react on the NO answer. Your response should be:

Code: Select all

 
<key>NO_PIOLTS_LOUNGE</key> 
<string>No games of hazard for me, I'd rather check out the piolts lounge</string> 


Make sure your OXP is loaded after HoOpy and you use the same keyname so it overwrites the original. HoOpy will never be triggered by the NO and your OXP is triggered by the "NO_PIOLTS_LOUNGE" response.

EDIT: and leave the mission_varriables as HoOpy intended, so change than as a NO would have done. And don't be supprised when things don't work when a new HoOpy release comes out. Fiddling with others OXP's should be avoided if possible.

Posted: Fri Oct 12, 2007 4:04 pm
by Arexack_Heretic
A reasonably safe way to acces other scripts is via the mission_variables they set.
Still, this leaves your OXP-dependent-OXP disables should the author of the primary OXP change it.

Safest way is to ask permission to use the content of said OXP and just add an alternate version of the hoopy or whatever.
No need to always have access to that lounge. 8)
Offcourse this only works for scripts that add something, where the stuff happens. If things are triggered in a standard station, you are back to using the mission_variables.
(I don't think you can hijack another OXP, while it's running its script...
:? )

Posted: Fri Oct 12, 2007 4:35 pm
by JensAyton
Arexack_Heretic wrote:
(I don't think you can hijack another OXP, while it's running its script...
:? )
That’s a thought… with JavaScript, it’s entirely possible for one script to rewrite another.

Posted: Fri Oct 12, 2007 8:28 pm
by Cmdr. Maegil
@Ahruman: thanks for the code, how long until it works?

Although perfect to add&use Your Add Here batches and racing team OXPs, how to do to choose a random sub-OXPs when there are two or more on the registry? The idea is both to create some ambient, and to open a new starting point for shady ("Assassins" could certainly have used the setting) or informal missions with reduced risk of interfering with the normal mission screens on the stations, and to have them appear only one at a time.


As for the need for flexibility, I was thinking on the lines of allowing someting of the sort:

Code: Select all

|-hOopyCasino.OXP
  |-BarTalkHoopy.OXP (default chit-chat)
  |-NoQuestionsAskedHoopy.OXP (randomly triggered add-on)
  |-PirateGuildHoopy.OXP (add-on)
  |  PirateGuildMission1.OXP (basic mission)
  |  PirateGuildMission2.OXP (sequel)
  |  PirateGuildFencers.OXP (add-on)
  |  PirateGuildWars.OXP (Mission)
  |    PirateGuildWarsVendettas.OXP (randomly triggered sequel)
  |    PirateGuildWarsPirateKings.OXP (sequel)
  |-ShadyPassagersHoopy.OXP (add-on)
  |  ShadyPassagersBatch1.OXP (basic batch)
  |  ShadyPassagersBatch2.OXP (add-on)
where each OXP after the basic/default could be done by different people.
Arexack_Heretic wrote:
Still, this leaves your OXP-dependent-OXP disables should the author of the primary OXP change it.
Safest way is to ask permission to use the content of said OXP and just add an alternate version of the hoopy or whatever.
Of course I wouldn't go against its creator, even if the license allows derivative work! Let's be civil about it. :wink:
Arexack_Heretic wrote:
No need to always have access to that lounge. 8)
Eric Walch wrote:

Code: Select all

<key>NO</key>
<string>No games of hazard for me, I'd rather check out the piolts lounge</string>
Will not work. Both OXP's could react on the NO answer. Your response should be:

Code: Select all

<key>NO_PIOLTS_LOUNGE</key>
<string>No games of hazard for me, I'd rather check out the piolts lounge</string>
Make sure your OXP is loaded after HoOpy and you use the same keyname so it overwrites the original. HoOpy will never be triggered by the NO and your OXP is triggered by the "NO_PIOLTS_LOUNGE" response.
So, the lounge would still be there to have a drink and enjoy the scenary, but you aren't forced to visit it.

...and, of course you'd need the parent OXPs to run an add-on!

Posted: Fri Oct 12, 2007 9:36 pm
by JensAyton
Cmdr. Maegil wrote:
Although perfect to add&use Your Add Here batches and racing team OXPs, how to do to choose a random sub-OXPs when there are two or more on the registry?

Code: Select all

let myRandomlyChosenScript = this.scriptRegistry[Math.floor(Math.random() * this.scriptRegistry.length)]
will select one of the registered scripts (assuming there’s at least one) at random. If you want to support different probabilities for each one, you would sum up the probabilities of all the scripts, and then:

Code: Select all

let selector = Math.random() * probabilitySum
let accumulator = 0
let selectedScript = null
for (let i = 0; i < scriptRegistry.length(); i++)
{
    accumulator += scriptRegistry[i].probability
    if (accumulator >= selector)
    {
        selectedScript = scriptRegistry[i]
        break
    }
}
(This is basically what Oolite does when it’s selecting a ship by role.)

For the nested hierarchy of scripts, there are two ways to go about it. Either extendable subscripts have their own script registry in the same way as the parent script, or the sub-subscripts check to see whether their immediate parent is installed. For instance, for PirateGuildWars in your example, you’d have something like:

Code: Select all

this.startUp = function()
{
    if (worldScripts["author-pirate-guild-hoopy"] != undefined)
    {
        // PirateGuildHoopy is installed, register self.
        this.setUpStuff()
    }
}
Note that this checks whether the parent script is installed, rather than checking whether it’s registered with hOopyCasino’s script. That wouldn’t work consistently since the order in which startUp() is called is not guaranteed, so PirateGuildHoopy might not be registered yet.

Posted: Fri Oct 12, 2007 10:49 pm
by LittleBear
In the Shaders Outpost Griff has posted a lovely model for a space bar. You could just use this instead of the HoOpy, script them to appear in Anarchies / fudals and trigger the missions when docked here. :wink:

With Assassins they contact you by Comms Message wherever you are, but you have to have a special piece of equipment fitted first, which you obtain by joining the Guild in an Anarchy. Sort of like Electra being rung by her Agent with a 'job'. :wink: