Difference between revisions of "Scripting Oolite with JavaScript"
m (→script.js File Template: adding the semicolon) |
Cholmondely (talk | contribs) m (→See Also: Updated link) |
||
(22 intermediate revisions by 8 users not shown) | |||
Line 1: | Line 1: | ||
− | [[Oolite]] 1.68 and later supports scripts written in [http://en.wikipedia.org/wiki/ECMAScript ECMAScript] (more commonly known as [http://en.wikipedia.org/wiki/JavaScript JavaScript]) in addition to its traditional model based on [[property lists]]. This page provides an overview of how JavaScript is used in Oolite. The page [[Oolite JavaScript Reference: object model]] provides reference for Oolite-specific objects and methods. The page [[Oolite JavaScript Reference: | + | [[Oolite]] 1.68 and later supports scripts written in [http://en.wikipedia.org/wiki/ECMAScript ECMAScript] (more commonly known as [http://en.wikipedia.org/wiki/JavaScript JavaScript]) in addition to its traditional model based on [[property lists]]. This page provides an overview of how JavaScript is used in Oolite. The page [[Oolite JavaScript Reference: object model]] provides reference for Oolite-specific objects and methods. The page [[Oolite JavaScript Reference: World script event handlers]] provides reference for the event handlers Oolite supports. The language standards and some tutorials can be found through the Wiki links provided above. The announcement [https://bb.oolite.space/viewtopic.php?p=130332#p130332 here] may be of use (scroll down to "Javascipt"). |
== Using JavaScript == | == Using JavaScript == | ||
− | Currently, JavaScript is supported for “worldScripts”, that is, as a replacement for scripts in ''script.plist'' and shipScripts, that acts as expansion for the ships AI. While a ''script.plist'' file may contain any number of separate scripts, a single JavaScript file may contain only one script. | + | Currently, JavaScript 6 is supported for “worldScripts”, that is, as a replacement for scripts in ''script.plist'' and shipScripts, that acts as expansion for the ships AI. While a ''script.plist'' file may contain any number of separate scripts, a single JavaScript file may contain only one script. |
− | If your OXP only uses one script, place a JavaScript file named ''script.js'' (or ''script.es'') in the OXP’s ''Config'' directory. If you wish to use multiple scripts, you may instead create file named ''world-scripts.plist'' in the ''Config'' directory. This [[property list]] file should consist of an array of worldScript names; the named scripts should exist in a directory named ''Scripts'' inside your OXP. As with most “atomic” files (files which cannot be merged), such script files must have a unique name to avoid conflicts with other OXPs. Using the | + | If your OXP only uses one script, place a JavaScript file named ''script.js'' (or ''script.es'') in the OXP’s ''Config'' directory. If you wish to use multiple scripts, you may instead create file named ''world-scripts.plist'' in the ''Config'' directory. This [[property list]] file should consist of an array of worldScript names; the named scripts should exist in a directory named ''Scripts'' inside your OXP. As with most “atomic” files (files which cannot be merged), such script files must have a unique name to avoid conflicts with other OXPs. Using the [[world-scripts.plist]] method, you can combine JavaScript, plist and OOS scripts however you wish. |
− | Whereas plist scripts are based on polling – all scripts are run at semi-regular intervals, whether they need to be or not – scripts written in JavaScript are “event driven” – different functions, or ''event handlers'', in the script are called in response to state changes in the game, or when other events of interest happen. For instance, <code>willExitWitchSpace</code> is called just before player exits witchspace, and <code>alertConditionChanged</code> is called whenever the alert condition changes. See the [[Oolite JavaScript event handler reference|event handler reference]] for a full list of handlers and when Oolite will call them. | + | Whereas plist scripts are based on polling – all scripts are run at semi-regular intervals, whether they need to be or not – scripts written in JavaScript are “event driven” – different functions, or ''event handlers'', in the script are called in response to state changes in the game, or when other events of interest happen. For instance, <code>willExitWitchSpace</code> is called just before player exits witchspace, and <code>alertConditionChanged</code> is called whenever the alert condition changes. |
+ | |||
+ | See the [[Oolite JavaScript event handler reference|event handler reference]] for a full list of handlers and when Oolite will call them. | ||
Line 14: | Line 16: | ||
Copy and paste this template into a file called script.js in the OXP Config directory. Ensure you change at least the Name value. '''Every script must have a unique name.''' If multiple scripts with the same name are encountered, Oolite will arbitrarily select one and discard the others. | Copy and paste this template into a file called script.js in the OXP Config directory. Ensure you change at least the Name value. '''Every script must have a unique name.''' If multiple scripts with the same name are encountered, Oolite will arbitrarily select one and discard the others. | ||
− | <pre>this.name = "My OXP Script" | + | Note: If you do not want to react to one of these events anyway you can fully remove that function. This list is complete now as it is easier for novice users to remove than to add. |
− | this.author = "Your Name Here" | + | |
− | this.copyright = "(C) | + | <pre>this.name = "My OXP Script"; |
− | this.licence = "CC-NC-by-SA 2.0" | + | this.author = "Your Name Here"; |
+ | this.copyright = "(C) 2021 Me."; | ||
+ | this.licence = "CC-NC-by-SA 2.0"; | ||
this.description = "This OXP doesn't do very much yet."; | this.description = "This OXP doesn't do very much yet."; | ||
− | this.version = "1.0 alpha 1" | + | this.version = "1.0 alpha 1"; |
"use strict"; | "use strict"; | ||
− | / | + | // Game State |
− | + | ||
− | " | + | this.gamePaused = function() |
− | + | { | |
+ | log(this.name, "gamePaused()"); | ||
+ | } | ||
+ | |||
+ | this.gameResumed = function() | ||
+ | { | ||
+ | log(this.name, "gameResumed()"); | ||
+ | } | ||
+ | |||
+ | this.playerWillSaveGame = function(reason) | ||
+ | { | ||
+ | log(this.name, "playerWillSaveGame("+reason+")"); | ||
+ | } | ||
+ | |||
this.startUp = function() | this.startUp = function() | ||
{ | { | ||
− | log(this.name, " | + | log(this.name, "startup()"); |
+ | } | ||
+ | |||
+ | this.startUpComplete = function() | ||
+ | { | ||
+ | log(this.name, "startupComplete()"); | ||
+ | } | ||
+ | |||
+ | // Docking | ||
+ | |||
+ | this.shipWillDockWithStation = function(station) | ||
+ | { | ||
+ | log(this.name, "shipWillDockWithStation("+station+")"); | ||
+ | } | ||
+ | |||
+ | this.shipDockedWithStation = function(station) | ||
+ | { | ||
+ | log(this.name, "shipDockedWithStation("+station+")"); | ||
+ | } | ||
+ | |||
+ | this.shipWillLaunchFromStation = function(stationLaunchedFrom) | ||
+ | { | ||
+ | log(this.name, "shipWillLaunchFromStation("+stationLaunchedFrom+")"); | ||
+ | } | ||
+ | |||
+ | this.shipLaunchedFromStation = function(stationLaunchedFrom) | ||
+ | { | ||
+ | log(this.name, "shipLaunchedFromStation("+stationLaunchedFrom+")"); | ||
+ | } | ||
+ | |||
+ | this.playerStartedAutoPilot = function() | ||
+ | { | ||
+ | log(this.name, "playerStartedAutoPilot()"); | ||
+ | } | ||
+ | |||
+ | this.playerCancelledAutoPilot = function() | ||
+ | { | ||
+ | log(this.name, "playerCancelledAutoPilot()"); | ||
+ | } | ||
+ | |||
+ | this.playerDockingClearanceCancelled = function() | ||
+ | { | ||
+ | log(this.name, "playerDockingClearanceCancelled()"); | ||
+ | } | ||
+ | |||
+ | this.playerDockingClearanceExpired = function() | ||
+ | { | ||
+ | log(this.name, "playerDockingClearanceExpired()"); | ||
+ | } | ||
+ | |||
+ | this.playerDockingClearanceGranted = function() | ||
+ | { | ||
+ | log(this.name, "playerDockingClearanceGranted()"); | ||
+ | } | ||
+ | |||
+ | this.playerDockingRefused = function() | ||
+ | { | ||
+ | log(this.name, "playerDockingRefused()"); | ||
+ | } | ||
+ | |||
+ | this.playerRequestedDockingClearance = function(message) | ||
+ | { | ||
+ | log(this.name, "playerRequestedDockingClearance("+message+")"); | ||
+ | } | ||
+ | |||
+ | this.playerRescuedEscapePod = function(fee, reason, occupant) | ||
+ | { | ||
+ | log(this.name, "playerRescuedEscapePod("+fee + ", " + reason + ", " + occupant+")"); | ||
+ | } | ||
+ | |||
+ | this.playerCompletedContract = function(type, result, fee, contract) | ||
+ | { | ||
+ | log(this.name, "playerCompletedContract("+fee + ", " + reason + ", " + occupant+")"); | ||
+ | } | ||
+ | |||
+ | this.playerEnteredContract = function(type, contract) | ||
+ | { | ||
+ | log(this.name, "playerEnteredContract("+type + ", " + contract + ")"); | ||
+ | } | ||
+ | |||
+ | // Witchspace jumps | ||
+ | |||
+ | this.playerStartedJumpCountdown = function(type, seconds) | ||
+ | { | ||
+ | log(this.name, "playerStartedJumpCountdown("+type + ", " + seconds + ")"); | ||
+ | } | ||
+ | |||
+ | this.playerCancelledJumpCountdown = function() | ||
+ | { | ||
+ | log(this.name, "playerCancelledJumpCountdown()"); | ||
+ | } | ||
+ | |||
+ | this.playerJumpFailed = function(reason) | ||
+ | { | ||
+ | log(this.name, "playerJumpFailed(" + reason + ")" ); | ||
+ | } | ||
+ | |||
+ | this.shipWillEnterWitchspace = function(cause, destination) | ||
+ | { | ||
+ | log(this.name, "shipWillEnterWitchspace(" + cause + ", " + destination + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipWillExitWitchspace = function() | ||
+ | { | ||
+ | log(this.name, "shipWillEnterWitchspace()"); | ||
+ | } | ||
+ | |||
+ | this.shipExitedWitchspace = function() | ||
+ | { | ||
+ | log(this.name, "shipExitedWitchspace()"); | ||
+ | } | ||
+ | |||
+ | this.playerEnteredNewGalaxy = function(galaxyNumber) | ||
+ | { | ||
+ | log(this.name, "playerEnteredNewGalaxy(" + galaxyNumber + ")"); | ||
+ | } | ||
+ | |||
+ | // Enter/Exit Aegis | ||
+ | this.shipEnteredStationAegis = function(station) | ||
+ | { | ||
+ | log(this.name, "shipEnteredStationAegis(" + station + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipExitedStationAegis = function(station) | ||
+ | { | ||
+ | log(this.name, "shipExitedStationAegis(" + station + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipEnteredPlanetaryVicinity = function(planet) | ||
+ | { | ||
+ | log(this.name, "shipEnteredPlanetaryVicinity(" + planet + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipExitedPlanetaryVicinity = function(planet) | ||
+ | { | ||
+ | log(this.name, "shipExitedPlanetaryVicinity(" + planet + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipApproachingPlanetSurface = function(planet) | ||
+ | { | ||
+ | log(this.name, "shipApproachingPlanetSurface(" + planet + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipLeavingPlanetSurface = function(planet) | ||
+ | { | ||
+ | log(this.name, "shipLeavingPlanetSurface(" + planet + ")"); | ||
+ | } | ||
+ | |||
+ | // Combat | ||
+ | |||
+ | this.alertConditionChanged = function(newCondition, oldCondition) | ||
+ | { | ||
+ | log(this.name, "alertConditionChanged(" + newCondition + ", " + oldCondition + ")"); | ||
+ | } | ||
+ | |||
+ | this.playerTargetedMissile = function(missile) | ||
+ | { | ||
+ | log(this.name, "playerTargetedMissile(" + missile + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipAttackedOther = function(other) | ||
+ | { | ||
+ | log(this.name, "shipAttackedOther(" + other + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipAttackedWithMissile = function(missile, whom) | ||
+ | { | ||
+ | log(this.name, "shipAttackedWithMissile(" + missile + ", " + whom + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipBeingAttacked = function(whom) | ||
+ | { | ||
+ | log(this.name, "shipBeingAttacked(" + whom + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipBeingAttackedByCloaked = function() | ||
+ | { | ||
+ | log(this.name, "shipBeingAttackedByCloaked()"); | ||
+ | } | ||
+ | |||
+ | this.shipKilledOther = function(whom, damageType) | ||
+ | { | ||
+ | log(this.name, "shipKilledOther(" + whom + ", " + damageType + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipReleasedEquipment = function(mine) | ||
+ | { | ||
+ | log(this.name, "shipReleasedEquipment(" + mine + ")"); | ||
+ | } | ||
+ | |||
+ | |||
+ | this.shipTargetDestroyed = function(target) | ||
+ | { | ||
+ | log(this.name, "shipTargetDestroyed(" + target + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipDied = function(whom, why) | ||
+ | { | ||
+ | log(this.name, "shipDied(" + whom + ", " + why + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipFiredMissile = function(missile, target) | ||
+ | { | ||
+ | log(this.name, "shipFiredMissile(" + missile + ", " + target + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipTargetLost = function(target) | ||
+ | { | ||
+ | log(this.name, "shipTargetLost(" + target + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipTargetCloaked = function() | ||
+ | { | ||
+ | log(this.name, "shipTargetCloaked()"); | ||
+ | } | ||
+ | |||
+ | this.weaponsSystemsToggled = function(state) | ||
+ | { | ||
+ | log(this.name, "weaponsSystemsToggled(" + state + ")"); | ||
+ | } | ||
+ | |||
+ | // Equipment and Cargo | ||
+ | |||
+ | this.equipmentAdded = function(equipmentKey) | ||
+ | { | ||
+ | log(this.name, "equipmentAdded(" + equipmentKey + ")"); | ||
+ | } | ||
+ | |||
+ | this.equipmentDamaged = function(equipment) | ||
+ | { | ||
+ | log(this.name, "equipmentDamaged(" + equipment + ")"); | ||
+ | } | ||
+ | |||
+ | this.equipmentRemoved = function(equipmentKey) | ||
+ | { | ||
+ | log(this.name, "equipmentRemoved(" + equipmentKey + ")"); | ||
+ | } | ||
+ | |||
+ | this.equipmentRepaired = function(equipment) | ||
+ | { | ||
+ | log(this.name, "equipmentRepaired(" + equipment + ")"); | ||
+ | } | ||
+ | |||
+ | this.playerBoughtCargo = function(commodity, units, price) | ||
+ | { | ||
+ | log(this.name, "playerBoughtCargo(" + commodity + ", " + units + ", " + price + ")"); | ||
+ | } | ||
+ | |||
+ | this.playerBoughtEquipment = function(equipment, paid) | ||
+ | { | ||
+ | log(this.name, "playerBoughtEquipment(" + equipment + ", " + paid + ")"); | ||
+ | } | ||
+ | |||
+ | this.playerBoughtNewShip = function(ship, price) | ||
+ | { | ||
+ | log(this.name, "playerBoughtNewShip(" + ship + ", " + price + ")"); | ||
+ | } | ||
+ | |||
+ | this.playerChangedPrimedEquipment = function(equipmentKey) | ||
+ | { | ||
+ | log(this.name, "playerChangedPrimedEquipment(" + equipmentKey + ")"); | ||
+ | } | ||
+ | |||
+ | this.playerReplacedShip = function(ship) | ||
+ | { | ||
+ | log(this.name, "playerReplacedShip(" + ship + ")"); | ||
+ | } | ||
+ | |||
+ | this.playerSoldCargo = function(commodity, units, price) | ||
+ | { | ||
+ | log(this.name, "playerSoldCargo(" + commodity + ", " + units + ", " + price + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipScoopedFuel = function() | ||
+ | { | ||
+ | log(this.name, "shipScoopedFuel()"); | ||
+ | } | ||
+ | |||
+ | this.shipScoopedOther = function(whom) | ||
+ | { | ||
+ | log(this.name, "shipScoopedOther(" + whom + ")"); | ||
+ | } | ||
+ | |||
+ | this.playerWillBuyNewShip = function(dataKey, shipyard, price, tradeIn) | ||
+ | { | ||
+ | log(this.name, "playerWillBuyNewShip(" + dataKey + ", " + shipyard + ", " + price + ", " + tradeIn + ")"); | ||
+ | } | ||
+ | |||
+ | this.playerWillReplaceShip = function(dataKey) | ||
+ | { | ||
+ | log(this.name, "playerWillReplaceShip(" + dataKey + ")"); | ||
+ | } | ||
+ | |||
+ | // Other | ||
+ | |||
+ | this.chartHightlightModeChanged = function(newMode) | ||
+ | { | ||
+ | log(this.name, "chartHightlightModeChanged(" + newMode + ")"); | ||
+ | } | ||
+ | |||
+ | this.compassTargetChanged = function(whom, mode) | ||
+ | { | ||
+ | log(this.name, "compassTargetChanged(" + whom + ", " + mode + ")"); | ||
+ | } | ||
+ | |||
+ | this.dayChanged = function(newday) | ||
+ | { | ||
+ | log(this.name, "dayChanged(" + newday + ")"); | ||
+ | } | ||
+ | |||
+ | this.escapePodSequenceOver = function() | ||
+ | { | ||
+ | log(this.name, "escapePodSequenceOver()"); | ||
+ | } | ||
+ | |||
+ | this.guiScreenChanged = function(to, from) | ||
+ | { | ||
+ | log(this.name, "guiScreenChanged(" + to + ", " + from + ")"); | ||
+ | } | ||
+ | |||
+ | this.guiScreenWillChange = function(to, from) | ||
+ | { | ||
+ | log(this.name, "guiScreenWillChange(" + to + ", " + from + ")"); | ||
+ | } | ||
+ | |||
+ | this.infoSystemChanged = function(to, from) | ||
+ | { | ||
+ | log(this.name, "infoSystemChanged(" + to + ", " + from + ")"); | ||
+ | } | ||
+ | |||
+ | this.infoSystemWillChange = function(to, from) | ||
+ | { | ||
+ | log(this.name, "infoSystemWillChange(" + to + ", " + from + ")"); | ||
+ | } | ||
+ | |||
+ | this.mfdKeyChanged = function(activeMFD, mfdKey) | ||
+ | { | ||
+ | log(this.name, "mfdKeyChanged(" + activeMFD + ", " + mfdKey + ")"); | ||
+ | } | ||
+ | |||
+ | this.missionChoiceWasReset= function() | ||
+ | { | ||
+ | log(this.name, "missionChoiceWasReset()"); | ||
} | } | ||
+ | |||
+ | this.missionScreenEnded = function() | ||
+ | { | ||
+ | log(this.name, "missionScreenEnded()"); | ||
+ | } | ||
+ | |||
+ | this.missionScreenOpportunity= function() | ||
+ | { | ||
+ | log(this.name, "missionScreenOpportunity()"); | ||
+ | } | ||
+ | |||
+ | this.reportScreenEnded = function() | ||
+ | { | ||
+ | log(this.name, "reportScreenEnded()"); | ||
+ | } | ||
+ | |||
+ | this.selectedMFDChanged = function(activeMFD) | ||
+ | { | ||
+ | log(this.name, "selectedMFDChanged(" + activeMFD + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipCollided = function(otherShip) | ||
+ | { | ||
+ | log(this.name, "shipCollided(" + otherShip + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipSpawned = function(ship) | ||
+ | { | ||
+ | log(this.name, "shipSpawned(" + ship + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipLaunchedEscapePod = function(escapepod) | ||
+ | { | ||
+ | log(this.name, "shipLaunchedEscapePod(" + escapepod + ")"); | ||
+ | } | ||
+ | |||
+ | |||
+ | this.systemInformationChanged = function(galaxy,system,key,newValue) | ||
+ | { | ||
+ | log(this.name, "systemInformationChanged(" + galaxy + ", " + system + ", " + key + ", " + newValue + ")"); | ||
+ | } | ||
+ | |||
+ | this.viewDirectionChanged = function(viewString) | ||
+ | { | ||
+ | log(this.name, "viewDirectionChanged(" + viewString + ")"); | ||
+ | } | ||
+ | |||
+ | this.shipTakingDamage = function() | ||
+ | { | ||
+ | log(this.name, "shipTakingDamage()"); | ||
+ | } | ||
+ | |||
</pre> | </pre> | ||
+ | |||
+ | === AppleMacs === | ||
+ | ... Strict semicolon parsing is a Mac thing. Windows and Linux use GNUstep to parse plists and that apparently is more relaxed syntax-wise than whatever it is that the Mac uses. [[User:Another_commander|Another_commander]] from [https://bb.oolite.space/viewtopic.php?p=262814#p262814 A possible solution?] (2018). | ||
+ | |||
+ | == What your computer will do with the Javascript == | ||
+ | The JavaScript is not interpreted directly, but rather is compiled to bytecode at load time. (The compiled bytecode is stored in Oolite’s cache. The exception is the eval() function, which compiles and then runs its argument, but there should be no reason to use this in Oolite except in the debug console script.) Any overhead related to parsing is thus a one-time cost. However, as it’s a completely dynamically typed language, opportunities for type-based optimizations are very limited. On the other hand, some types such as “small” integers (in the range ±1 billion and a bit) can be special-cased easily at run time. ([https://bb.oolite.space/viewtopic.php?p=47013#p47013 Jens Ayton/Ahruman (2008)] | ||
+ | |||
+ | == Understanding Javascript == | ||
+ | JensAyton/Ahruman (2011): I’ve been talking recently about prototype chains and constructors and how everyone should have been spelling system with a lowercase s, except when they shouldn’t, so I thought it would be nice to at least skim through the underlying concepts from an Oolitey perspective. | ||
+ | |||
+ | All but the most primitive programming languages have some sort of type system, a way of formalizing the fact that while text, numbers and spaceships are all buckets of bytes to the machine, they mean different things at the higher level of abstraction where the programmer works. Types can be associated with labels, such as variables and properties, or with values; in JavaScript, they’re always associated with values. (This is known as ''dynamic typing'', as opposed to ''static typing''.) | ||
+ | |||
+ | JavaScript almost has a very simple, uniform type system, in which every value is either an object, null (representing “no value”), or undefined (indicating a variable or property does not exist.) In actuality JavaScript’s type system is far more complex for pragmatic reasons, including performance, browser security concerns and, not least, the fact that the entire language was designed and implemented in one week by one guy. But all the things Oolite defines are ''objects'', so we can mostly pretend the simple type system actually exists. | ||
+ | |||
+ | In JavaScript, an ''object'' is a collection of ''properties''. Each property has an ''identifier'' (which can be a string or a number), some attributes specifying things like whether it can be modified, and a value. If the value is a function (or, technically, any callable object), it’s called a ''method''. There is one special rule for methods: if you call a function by referring to it as a property of an object, that object is accessible as this from within the function. For example, if you call someObject.method(), this will refer to the same object as someObject while method() is running. The function itself is not tied to the object, and the same function can be a method of different objects, even under different names. | ||
+ | |||
+ | One of the complications that would be nice to gloss over, but is going to be quite important, is that ''properties can be faked''. Normally, you set the value of a given property and it stays set until you change it, but there is an alternative: a property can be based on ''accessors'', where a function is called to get the value (and, optionally, a second function is called to set the value). Historically, it was only possible for host objects – i.e., ones defined by Oolite or the JavaScript engine – to create such properties, but there is a SpiderMonkey extension to do it from scripts and ECMAScript 5th Edition adds a new, standardized way. Host objects mostly do it for performance; for instance, instead of setting the position property of each ship’s JavaScript representation every frame, and creating a new JavaScript Vector3D each time, it’s done on the fly on those occasions where a script actually asks for it. | ||
+ | |||
+ | The model as described so far is strictly sufficient, but it does have an important flaw: to create multiple similar objects, it would be necessary to create each object and then set each property for it, even if it’s a default value or an accessor-backed property. Most object-oriented programming systems address this problem using a concept called a class, where each object belongs to a fixed class and each class represents one possible set of properties, and classes can be based on other classes forming an inheritance hierarchy. JavaScript uses a simpler yet more flexible model, in which each object can inherit behaviour from another object, known as its ''prototype''. If you attempt to access a property of an object, but the property does not exist, the prototype is consulted, and if necessary its prototype in turn; this is known as following the prototype chain. | ||
+ | |||
+ | Say o is an object with (initially) no properties, and p is o’s prototype, also with no properties. If you set p.foo to 3, and then request o.foo, you get 3. Further changes to p.foo are also reflected in o.foo. If you set o.bar to 5, then request o.bar, you get 5, but p.bar is still not defined. | ||
+ | |||
+ | If you set o.foo to 7, then o gets its own foo property which shadows p’s. From then on, the two are distinct, unless you delete o.foo, at which point it again inherits p.foo. | ||
+ | |||
+ | Transferring this to Oolite objects, it should come to no surprise that all ships have, in their prototype chains, an object which defines the common properties of ships; we can call it the ''[[Oolite JavaScript Reference: Ship|Ship Prototype]]''. The Ship Prototype inherits behaviour from the ''[[Oolite JavaScript Reference: Entity|Entity Prototype]]'', which defines the common properties of all entities (for instance, both ships and planets have a position property, which is an accessor-backed property of the Entity Prototype). | ||
+ | |||
+ | In order to create an object with a specific prototype from within JavaScript, you use a ''constructor'', which is a function designed to set up a new object in conjunction with the new operator. In Oolite, you’ll most often use the constructors [[Oolite JavaScript Reference: Vector3D|Vector3D]] and [[Oolite JavaScript Reference: Vector3D|Timer]], as in this.v = new Vector3D(1, 0, 0);. When an object is created using new, its prototype is set to the value of the constructor’s prototype property. Note that this is not the constructor’s prototype, but a normal property whose name is prototype. Here is an example of how you might use this to define your own object hierarchy: | ||
+ | |||
+ | var p = { foo: 3 }; | ||
+ | function P() | ||
+ | { | ||
+ | this.bar = 5; | ||
+ | } | ||
+ | P.prototype = p; | ||
+ | |||
+ | o = new P; | ||
+ | // o is now an object whose prototype is p. | ||
+ | // In ECMAv5 and trunk (even oldjs builds) you can test this with Object.getPrototypeOf(o) == p. | ||
+ | // In earlier versions, you can use o.__proto__ == p, which is a SpiderMonkey extension. | ||
+ | |||
+ | log(o.foo); // 3 | ||
+ | log(o.bar); // 5 | ||
+ | p.foo = 4; | ||
+ | log(o.foo); // 4 | ||
+ | |||
+ | // By the way, the new operator also set o’s “constructor” property to the function P. | ||
+ | |||
+ | The same relationships are supposed to apply to Oolite-defined objects, and in trunk they do. For example, Object.getPrototypeOf(player.ship) == PlayerShip.prototype should be true, and now is. For that matter, new PlayerShip also “works”, by throwing an exception telling you you’re not allowed to make your own players. If you follow the prototype chain, PlayerShip.prototype’s prototype is equal to Ship.prototype, Ship.prototype’s prototype is Entity.prototype, Entity.prototype’s prototype is Object.prototype and Object.prototype’s prototype is null. | ||
+ | |||
+ | In Oolite v.1.74 and earlier, most of the type names that should have been constructors instead referred to prototypes, because I was Doing It Wrong. This lead to a bunch of problems which I’ve sort of muddled through, such as compatibility methods for some objects unexpectedly becoming methods of Object.prototype. Another side effect was that you could call methods (or accessor-backed properties) on the prototypes by referring to the purported constructor name. For cases where only one object can exist, like the player ship, the method implementations ignore the this parameter and use the native Objective-C object directly, which is why you could call PlayerShip.awardCargo() instead of player.ship.awardCargo(). (In Oolite v.1.75, you could instead call PlayerShip.prototype.awardCargo(), but you’re much less likely to end up in that situation by mistake. Also, it’s very definitely not guaranteed to work in future.) | ||
+ | |||
+ | So, let’s look at the root of the problem, namely the naming of Oolite JavaScript Reference pages. Let’s take [[Oolite JavaScript Reference: System|System]] as an example. The name is System with a capital S, referring to the name of the constructor – which, to the extent there is such a thing in JavaScript, is the type name. The second sentence of the introduction tells you that there’s one instance of System, and it’s available as the global variable system. (A “global variable” is one that’s visible to all code without any special qualification, unless there’s a local variable “shadowing” it. In JavaScript, global variables are actually properties of a “global object”, which is why it says “global property.”) | ||
+ | |||
+ | The bulk of the article is divided into three sections, Properties, Methods and Static Methods. All this wiki's Javascript reference pages use this structure, although they elide empty sections. The terminology is wrong, and refers to concepts from C++ rather than JavaScript; it’s written that way because that’s how they’re referred to in the SpiderMonkey programming interface and because I started writing the documentation before I fully grokked the language. | ||
+ | |||
+ | In actuality, the Properties section contains non-method properties of the System Prototype – which in Oolite v.1.75 is System.prototype but in earlier versions is accidentally System itself – and which are inherited by instances, in this case system. Methods is similar, for properties that happen to be functions. Static Methods contains methods that don’t apply to a particular instance – in this case, functions dealing with other systems – and are attached to the constructor. | ||
+ | |||
+ | This distinction may be clearer if we look at a type which has more than one instance and also has “static methods”, namely [[Oolite JavaScript Reference: Vector3D|Vector3D]]. It makes sense to call someEntity.position.add([1, 0, 0]); this adds the vector (1, 0, 0) to the vector someEntity.position (following the special rule that arrays can be automatically converted to vectors). It doesn’t make sense to call Vector3D.add([1, 0, 0]), because Vector3D doesn’t refer to any specific vector, and in fact it raises an exception, “Vector3D.add is not a function”, because even in earlier versions Vector3D is the constructor rather than the prototype. (In Oolite v.1.74, Vector3D.prototype.add([1, 0, 0]) returns undefined; in trunk, it returns (1, 0, 0). In future versions, it might do some other thing, whatever seems the most efficient non-crashing behaviour.) On the other hand, Vector3D.randomDirectionAndLength() creates a new vector that isn’t related to any existing vector; it doesn’t make sense to call player.ship.position.randomDirectionAndLength(). | ||
+ | |||
+ | As I said, the terminology is all wrong, but “fixing” it wouldn’t really make things better. A new Oolite scripter seeing the categories Properties of System.prototype and Properties of System wouldn’t be better off than now. Explaining the distinction in terms specific to each type at the start of the page would be a horrible, mind-damaging thing that would make it much harder for people to actually understand (so please don’t “helpfully” do that). What’s needed is a simple yet basically correct summary of this information, but I’m pathologically incapable of writing it. | ||
+ | |||
+ | As a reward for reading all the way through this short and simplified summary, here’s a fun function you can copy straight into the console. It should work in any version of Oolite which actually has a console: | ||
+ | |||
+ | this.protoChain = function (object) | ||
+ | { | ||
+ | function pr(v) | ||
+ | { | ||
+ | // Get prototype of v, boxing it if it’s a primitive. | ||
+ | if (typeof Object.getPrototypeOf == "function") return Object.getPrototypeOf(new Object(v)); | ||
+ | else return v.__proto__; | ||
+ | } | ||
+ | var result = "", first = true; | ||
+ | for (;;) | ||
+ | { | ||
+ | var proto = pr(object); | ||
+ | if (!proto) return result; | ||
+ | if (!first) result += ": "; | ||
+ | else first = false; | ||
+ | result += proto.constructor.name || "<anonymous>"; | ||
+ | object = proto; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | In trunk, protoChain(player.ship) returns “PlayerShip: Ship: Entity: Object”. In Oolite v.1.74, you get “Object: Object: Object: Object”, which is distinctly wrong albeit mildly amusing to fans of Catch-22. In either, protoChain(new Vector3D) returns “Vector3D: Object”, and protoChain([]) returns “Array: Object”. It also deals with (''i.e.'', correctly lies about) primitive values; protoChain(5) returns “Number: Object”. | ||
+ | |||
+ | ''Note that Ahruman's masterpiece versions of Oolite came after this article - v.1.76.1 & v.1.77.1: the Article is [https://bb.oolite.space/viewtopic.php?f=4&t=8968 Understanding JavaScript, maybe]'' | ||
== See Also == | == See Also == | ||
− | * [https://developer.mozilla.org/en/JavaScript/Reference Mozilla JavaScript reference pages] | + | * [[Oolite Javascript basics]] - 2022 essay by [[User:Massively Locked|Massively Locked]] |
+ | * [https://developer.mozilla.org/en/JavaScript/Reference Mozilla JavaScript reference pages] (check for JavaScript 6) | ||
* [[Oolite JavaScript event handler reference]] | * [[Oolite JavaScript event handler reference]] | ||
* [[Oolite JavaScript object model]] | * [[Oolite JavaScript object model]] | ||
Line 41: | Line 539: | ||
* [[Javascript Operators]] | * [[Javascript Operators]] | ||
* [[Handling OXP Dependencies with JavaScript]] | * [[Handling OXP Dependencies with JavaScript]] | ||
+ | * [[Javascript optimization tips]] | ||
+ | * [https://bb.oolite.space/viewtopic.php?p=221738#p221738 Writing a debug message into the latest.log] (2014) | ||
+ | |||
+ | * [http://blog.wolfire.com/2009/07/linear-algebra-for-game-developers-part-1/ Linear algebra for game developers] (First of 4 parts, recommended by Ahruman in 2010) | ||
+ | |||
+ | === History === | ||
+ | * [https://bb.oolite.space/viewtopic.php?f=6&t=2784 Anyone want to write scripts using JavaScript?] ([[David Taylor|Dajt]], 2007) | ||
+ | |||
+ | === Updating Javascript/Spidermonkey === | ||
+ | * [https://bb.oolite.space/viewtopic.php?p=275357#p275357 Issues with updating] (2021) | ||
+ | [[Category:Oolite]] | ||
[[Category:Oolite scripting]] | [[Category:Oolite scripting]] |
Latest revision as of 09:11, 30 June 2024
Oolite 1.68 and later supports scripts written in ECMAScript (more commonly known as JavaScript) in addition to its traditional model based on property lists. This page provides an overview of how JavaScript is used in Oolite. The page Oolite JavaScript Reference: object model provides reference for Oolite-specific objects and methods. The page Oolite JavaScript Reference: World script event handlers provides reference for the event handlers Oolite supports. The language standards and some tutorials can be found through the Wiki links provided above. The announcement here may be of use (scroll down to "Javascipt").
Contents
Using JavaScript
Currently, JavaScript 6 is supported for “worldScripts”, that is, as a replacement for scripts in script.plist and shipScripts, that acts as expansion for the ships AI. While a script.plist file may contain any number of separate scripts, a single JavaScript file may contain only one script.
If your OXP only uses one script, place a JavaScript file named script.js (or script.es) in the OXP’s Config directory. If you wish to use multiple scripts, you may instead create file named world-scripts.plist in the Config directory. This property list file should consist of an array of worldScript names; the named scripts should exist in a directory named Scripts inside your OXP. As with most “atomic” files (files which cannot be merged), such script files must have a unique name to avoid conflicts with other OXPs. Using the world-scripts.plist method, you can combine JavaScript, plist and OOS scripts however you wish.
Whereas plist scripts are based on polling – all scripts are run at semi-regular intervals, whether they need to be or not – scripts written in JavaScript are “event driven” – different functions, or event handlers, in the script are called in response to state changes in the game, or when other events of interest happen. For instance, willExitWitchSpace
is called just before player exits witchspace, and alertConditionChanged
is called whenever the alert condition changes.
See the event handler reference for a full list of handlers and when Oolite will call them.
script.js File Template
Copy and paste this template into a file called script.js in the OXP Config directory. Ensure you change at least the Name value. Every script must have a unique name. If multiple scripts with the same name are encountered, Oolite will arbitrarily select one and discard the others.
Note: If you do not want to react to one of these events anyway you can fully remove that function. This list is complete now as it is easier for novice users to remove than to add.
this.name = "My OXP Script"; this.author = "Your Name Here"; this.copyright = "(C) 2021 Me."; this.licence = "CC-NC-by-SA 2.0"; this.description = "This OXP doesn't do very much yet."; this.version = "1.0 alpha 1"; "use strict"; // Game State this.gamePaused = function() { log(this.name, "gamePaused()"); } this.gameResumed = function() { log(this.name, "gameResumed()"); } this.playerWillSaveGame = function(reason) { log(this.name, "playerWillSaveGame("+reason+")"); } this.startUp = function() { log(this.name, "startup()"); } this.startUpComplete = function() { log(this.name, "startupComplete()"); } // Docking this.shipWillDockWithStation = function(station) { log(this.name, "shipWillDockWithStation("+station+")"); } this.shipDockedWithStation = function(station) { log(this.name, "shipDockedWithStation("+station+")"); } this.shipWillLaunchFromStation = function(stationLaunchedFrom) { log(this.name, "shipWillLaunchFromStation("+stationLaunchedFrom+")"); } this.shipLaunchedFromStation = function(stationLaunchedFrom) { log(this.name, "shipLaunchedFromStation("+stationLaunchedFrom+")"); } this.playerStartedAutoPilot = function() { log(this.name, "playerStartedAutoPilot()"); } this.playerCancelledAutoPilot = function() { log(this.name, "playerCancelledAutoPilot()"); } this.playerDockingClearanceCancelled = function() { log(this.name, "playerDockingClearanceCancelled()"); } this.playerDockingClearanceExpired = function() { log(this.name, "playerDockingClearanceExpired()"); } this.playerDockingClearanceGranted = function() { log(this.name, "playerDockingClearanceGranted()"); } this.playerDockingRefused = function() { log(this.name, "playerDockingRefused()"); } this.playerRequestedDockingClearance = function(message) { log(this.name, "playerRequestedDockingClearance("+message+")"); } this.playerRescuedEscapePod = function(fee, reason, occupant) { log(this.name, "playerRescuedEscapePod("+fee + ", " + reason + ", " + occupant+")"); } this.playerCompletedContract = function(type, result, fee, contract) { log(this.name, "playerCompletedContract("+fee + ", " + reason + ", " + occupant+")"); } this.playerEnteredContract = function(type, contract) { log(this.name, "playerEnteredContract("+type + ", " + contract + ")"); } // Witchspace jumps this.playerStartedJumpCountdown = function(type, seconds) { log(this.name, "playerStartedJumpCountdown("+type + ", " + seconds + ")"); } this.playerCancelledJumpCountdown = function() { log(this.name, "playerCancelledJumpCountdown()"); } this.playerJumpFailed = function(reason) { log(this.name, "playerJumpFailed(" + reason + ")" ); } this.shipWillEnterWitchspace = function(cause, destination) { log(this.name, "shipWillEnterWitchspace(" + cause + ", " + destination + ")"); } this.shipWillExitWitchspace = function() { log(this.name, "shipWillEnterWitchspace()"); } this.shipExitedWitchspace = function() { log(this.name, "shipExitedWitchspace()"); } this.playerEnteredNewGalaxy = function(galaxyNumber) { log(this.name, "playerEnteredNewGalaxy(" + galaxyNumber + ")"); } // Enter/Exit Aegis this.shipEnteredStationAegis = function(station) { log(this.name, "shipEnteredStationAegis(" + station + ")"); } this.shipExitedStationAegis = function(station) { log(this.name, "shipExitedStationAegis(" + station + ")"); } this.shipEnteredPlanetaryVicinity = function(planet) { log(this.name, "shipEnteredPlanetaryVicinity(" + planet + ")"); } this.shipExitedPlanetaryVicinity = function(planet) { log(this.name, "shipExitedPlanetaryVicinity(" + planet + ")"); } this.shipApproachingPlanetSurface = function(planet) { log(this.name, "shipApproachingPlanetSurface(" + planet + ")"); } this.shipLeavingPlanetSurface = function(planet) { log(this.name, "shipLeavingPlanetSurface(" + planet + ")"); } // Combat this.alertConditionChanged = function(newCondition, oldCondition) { log(this.name, "alertConditionChanged(" + newCondition + ", " + oldCondition + ")"); } this.playerTargetedMissile = function(missile) { log(this.name, "playerTargetedMissile(" + missile + ")"); } this.shipAttackedOther = function(other) { log(this.name, "shipAttackedOther(" + other + ")"); } this.shipAttackedWithMissile = function(missile, whom) { log(this.name, "shipAttackedWithMissile(" + missile + ", " + whom + ")"); } this.shipBeingAttacked = function(whom) { log(this.name, "shipBeingAttacked(" + whom + ")"); } this.shipBeingAttackedByCloaked = function() { log(this.name, "shipBeingAttackedByCloaked()"); } this.shipKilledOther = function(whom, damageType) { log(this.name, "shipKilledOther(" + whom + ", " + damageType + ")"); } this.shipReleasedEquipment = function(mine) { log(this.name, "shipReleasedEquipment(" + mine + ")"); } this.shipTargetDestroyed = function(target) { log(this.name, "shipTargetDestroyed(" + target + ")"); } this.shipDied = function(whom, why) { log(this.name, "shipDied(" + whom + ", " + why + ")"); } this.shipFiredMissile = function(missile, target) { log(this.name, "shipFiredMissile(" + missile + ", " + target + ")"); } this.shipTargetLost = function(target) { log(this.name, "shipTargetLost(" + target + ")"); } this.shipTargetCloaked = function() { log(this.name, "shipTargetCloaked()"); } this.weaponsSystemsToggled = function(state) { log(this.name, "weaponsSystemsToggled(" + state + ")"); } // Equipment and Cargo this.equipmentAdded = function(equipmentKey) { log(this.name, "equipmentAdded(" + equipmentKey + ")"); } this.equipmentDamaged = function(equipment) { log(this.name, "equipmentDamaged(" + equipment + ")"); } this.equipmentRemoved = function(equipmentKey) { log(this.name, "equipmentRemoved(" + equipmentKey + ")"); } this.equipmentRepaired = function(equipment) { log(this.name, "equipmentRepaired(" + equipment + ")"); } this.playerBoughtCargo = function(commodity, units, price) { log(this.name, "playerBoughtCargo(" + commodity + ", " + units + ", " + price + ")"); } this.playerBoughtEquipment = function(equipment, paid) { log(this.name, "playerBoughtEquipment(" + equipment + ", " + paid + ")"); } this.playerBoughtNewShip = function(ship, price) { log(this.name, "playerBoughtNewShip(" + ship + ", " + price + ")"); } this.playerChangedPrimedEquipment = function(equipmentKey) { log(this.name, "playerChangedPrimedEquipment(" + equipmentKey + ")"); } this.playerReplacedShip = function(ship) { log(this.name, "playerReplacedShip(" + ship + ")"); } this.playerSoldCargo = function(commodity, units, price) { log(this.name, "playerSoldCargo(" + commodity + ", " + units + ", " + price + ")"); } this.shipScoopedFuel = function() { log(this.name, "shipScoopedFuel()"); } this.shipScoopedOther = function(whom) { log(this.name, "shipScoopedOther(" + whom + ")"); } this.playerWillBuyNewShip = function(dataKey, shipyard, price, tradeIn) { log(this.name, "playerWillBuyNewShip(" + dataKey + ", " + shipyard + ", " + price + ", " + tradeIn + ")"); } this.playerWillReplaceShip = function(dataKey) { log(this.name, "playerWillReplaceShip(" + dataKey + ")"); } // Other this.chartHightlightModeChanged = function(newMode) { log(this.name, "chartHightlightModeChanged(" + newMode + ")"); } this.compassTargetChanged = function(whom, mode) { log(this.name, "compassTargetChanged(" + whom + ", " + mode + ")"); } this.dayChanged = function(newday) { log(this.name, "dayChanged(" + newday + ")"); } this.escapePodSequenceOver = function() { log(this.name, "escapePodSequenceOver()"); } this.guiScreenChanged = function(to, from) { log(this.name, "guiScreenChanged(" + to + ", " + from + ")"); } this.guiScreenWillChange = function(to, from) { log(this.name, "guiScreenWillChange(" + to + ", " + from + ")"); } this.infoSystemChanged = function(to, from) { log(this.name, "infoSystemChanged(" + to + ", " + from + ")"); } this.infoSystemWillChange = function(to, from) { log(this.name, "infoSystemWillChange(" + to + ", " + from + ")"); } this.mfdKeyChanged = function(activeMFD, mfdKey) { log(this.name, "mfdKeyChanged(" + activeMFD + ", " + mfdKey + ")"); } this.missionChoiceWasReset= function() { log(this.name, "missionChoiceWasReset()"); } this.missionScreenEnded = function() { log(this.name, "missionScreenEnded()"); } this.missionScreenOpportunity= function() { log(this.name, "missionScreenOpportunity()"); } this.reportScreenEnded = function() { log(this.name, "reportScreenEnded()"); } this.selectedMFDChanged = function(activeMFD) { log(this.name, "selectedMFDChanged(" + activeMFD + ")"); } this.shipCollided = function(otherShip) { log(this.name, "shipCollided(" + otherShip + ")"); } this.shipSpawned = function(ship) { log(this.name, "shipSpawned(" + ship + ")"); } this.shipLaunchedEscapePod = function(escapepod) { log(this.name, "shipLaunchedEscapePod(" + escapepod + ")"); } this.systemInformationChanged = function(galaxy,system,key,newValue) { log(this.name, "systemInformationChanged(" + galaxy + ", " + system + ", " + key + ", " + newValue + ")"); } this.viewDirectionChanged = function(viewString) { log(this.name, "viewDirectionChanged(" + viewString + ")"); } this.shipTakingDamage = function() { log(this.name, "shipTakingDamage()"); }
AppleMacs
... Strict semicolon parsing is a Mac thing. Windows and Linux use GNUstep to parse plists and that apparently is more relaxed syntax-wise than whatever it is that the Mac uses. Another_commander from A possible solution? (2018).
What your computer will do with the Javascript
The JavaScript is not interpreted directly, but rather is compiled to bytecode at load time. (The compiled bytecode is stored in Oolite’s cache. The exception is the eval() function, which compiles and then runs its argument, but there should be no reason to use this in Oolite except in the debug console script.) Any overhead related to parsing is thus a one-time cost. However, as it’s a completely dynamically typed language, opportunities for type-based optimizations are very limited. On the other hand, some types such as “small” integers (in the range ±1 billion and a bit) can be special-cased easily at run time. (Jens Ayton/Ahruman (2008)
Understanding Javascript
JensAyton/Ahruman (2011): I’ve been talking recently about prototype chains and constructors and how everyone should have been spelling system with a lowercase s, except when they shouldn’t, so I thought it would be nice to at least skim through the underlying concepts from an Oolitey perspective.
All but the most primitive programming languages have some sort of type system, a way of formalizing the fact that while text, numbers and spaceships are all buckets of bytes to the machine, they mean different things at the higher level of abstraction where the programmer works. Types can be associated with labels, such as variables and properties, or with values; in JavaScript, they’re always associated with values. (This is known as dynamic typing, as opposed to static typing.)
JavaScript almost has a very simple, uniform type system, in which every value is either an object, null (representing “no value”), or undefined (indicating a variable or property does not exist.) In actuality JavaScript’s type system is far more complex for pragmatic reasons, including performance, browser security concerns and, not least, the fact that the entire language was designed and implemented in one week by one guy. But all the things Oolite defines are objects, so we can mostly pretend the simple type system actually exists.
In JavaScript, an object is a collection of properties. Each property has an identifier (which can be a string or a number), some attributes specifying things like whether it can be modified, and a value. If the value is a function (or, technically, any callable object), it’s called a method. There is one special rule for methods: if you call a function by referring to it as a property of an object, that object is accessible as this from within the function. For example, if you call someObject.method(), this will refer to the same object as someObject while method() is running. The function itself is not tied to the object, and the same function can be a method of different objects, even under different names.
One of the complications that would be nice to gloss over, but is going to be quite important, is that properties can be faked. Normally, you set the value of a given property and it stays set until you change it, but there is an alternative: a property can be based on accessors, where a function is called to get the value (and, optionally, a second function is called to set the value). Historically, it was only possible for host objects – i.e., ones defined by Oolite or the JavaScript engine – to create such properties, but there is a SpiderMonkey extension to do it from scripts and ECMAScript 5th Edition adds a new, standardized way. Host objects mostly do it for performance; for instance, instead of setting the position property of each ship’s JavaScript representation every frame, and creating a new JavaScript Vector3D each time, it’s done on the fly on those occasions where a script actually asks for it.
The model as described so far is strictly sufficient, but it does have an important flaw: to create multiple similar objects, it would be necessary to create each object and then set each property for it, even if it’s a default value or an accessor-backed property. Most object-oriented programming systems address this problem using a concept called a class, where each object belongs to a fixed class and each class represents one possible set of properties, and classes can be based on other classes forming an inheritance hierarchy. JavaScript uses a simpler yet more flexible model, in which each object can inherit behaviour from another object, known as its prototype. If you attempt to access a property of an object, but the property does not exist, the prototype is consulted, and if necessary its prototype in turn; this is known as following the prototype chain.
Say o is an object with (initially) no properties, and p is o’s prototype, also with no properties. If you set p.foo to 3, and then request o.foo, you get 3. Further changes to p.foo are also reflected in o.foo. If you set o.bar to 5, then request o.bar, you get 5, but p.bar is still not defined.
If you set o.foo to 7, then o gets its own foo property which shadows p’s. From then on, the two are distinct, unless you delete o.foo, at which point it again inherits p.foo.
Transferring this to Oolite objects, it should come to no surprise that all ships have, in their prototype chains, an object which defines the common properties of ships; we can call it the Ship Prototype. The Ship Prototype inherits behaviour from the Entity Prototype, which defines the common properties of all entities (for instance, both ships and planets have a position property, which is an accessor-backed property of the Entity Prototype).
In order to create an object with a specific prototype from within JavaScript, you use a constructor, which is a function designed to set up a new object in conjunction with the new operator. In Oolite, you’ll most often use the constructors Vector3D and Timer, as in this.v = new Vector3D(1, 0, 0);. When an object is created using new, its prototype is set to the value of the constructor’s prototype property. Note that this is not the constructor’s prototype, but a normal property whose name is prototype. Here is an example of how you might use this to define your own object hierarchy:
var p = { foo: 3 }; function P() { this.bar = 5; } P.prototype = p;
o = new P; // o is now an object whose prototype is p. // In ECMAv5 and trunk (even oldjs builds) you can test this with Object.getPrototypeOf(o) == p. // In earlier versions, you can use o.__proto__ == p, which is a SpiderMonkey extension. log(o.foo); // 3 log(o.bar); // 5 p.foo = 4; log(o.foo); // 4 // By the way, the new operator also set o’s “constructor” property to the function P.
The same relationships are supposed to apply to Oolite-defined objects, and in trunk they do. For example, Object.getPrototypeOf(player.ship) == PlayerShip.prototype should be true, and now is. For that matter, new PlayerShip also “works”, by throwing an exception telling you you’re not allowed to make your own players. If you follow the prototype chain, PlayerShip.prototype’s prototype is equal to Ship.prototype, Ship.prototype’s prototype is Entity.prototype, Entity.prototype’s prototype is Object.prototype and Object.prototype’s prototype is null.
In Oolite v.1.74 and earlier, most of the type names that should have been constructors instead referred to prototypes, because I was Doing It Wrong. This lead to a bunch of problems which I’ve sort of muddled through, such as compatibility methods for some objects unexpectedly becoming methods of Object.prototype. Another side effect was that you could call methods (or accessor-backed properties) on the prototypes by referring to the purported constructor name. For cases where only one object can exist, like the player ship, the method implementations ignore the this parameter and use the native Objective-C object directly, which is why you could call PlayerShip.awardCargo() instead of player.ship.awardCargo(). (In Oolite v.1.75, you could instead call PlayerShip.prototype.awardCargo(), but you’re much less likely to end up in that situation by mistake. Also, it’s very definitely not guaranteed to work in future.)
So, let’s look at the root of the problem, namely the naming of Oolite JavaScript Reference pages. Let’s take System as an example. The name is System with a capital S, referring to the name of the constructor – which, to the extent there is such a thing in JavaScript, is the type name. The second sentence of the introduction tells you that there’s one instance of System, and it’s available as the global variable system. (A “global variable” is one that’s visible to all code without any special qualification, unless there’s a local variable “shadowing” it. In JavaScript, global variables are actually properties of a “global object”, which is why it says “global property.”)
The bulk of the article is divided into three sections, Properties, Methods and Static Methods. All this wiki's Javascript reference pages use this structure, although they elide empty sections. The terminology is wrong, and refers to concepts from C++ rather than JavaScript; it’s written that way because that’s how they’re referred to in the SpiderMonkey programming interface and because I started writing the documentation before I fully grokked the language.
In actuality, the Properties section contains non-method properties of the System Prototype – which in Oolite v.1.75 is System.prototype but in earlier versions is accidentally System itself – and which are inherited by instances, in this case system. Methods is similar, for properties that happen to be functions. Static Methods contains methods that don’t apply to a particular instance – in this case, functions dealing with other systems – and are attached to the constructor.
This distinction may be clearer if we look at a type which has more than one instance and also has “static methods”, namely Vector3D. It makes sense to call someEntity.position.add([1, 0, 0]); this adds the vector (1, 0, 0) to the vector someEntity.position (following the special rule that arrays can be automatically converted to vectors). It doesn’t make sense to call Vector3D.add([1, 0, 0]), because Vector3D doesn’t refer to any specific vector, and in fact it raises an exception, “Vector3D.add is not a function”, because even in earlier versions Vector3D is the constructor rather than the prototype. (In Oolite v.1.74, Vector3D.prototype.add([1, 0, 0]) returns undefined; in trunk, it returns (1, 0, 0). In future versions, it might do some other thing, whatever seems the most efficient non-crashing behaviour.) On the other hand, Vector3D.randomDirectionAndLength() creates a new vector that isn’t related to any existing vector; it doesn’t make sense to call player.ship.position.randomDirectionAndLength().
As I said, the terminology is all wrong, but “fixing” it wouldn’t really make things better. A new Oolite scripter seeing the categories Properties of System.prototype and Properties of System wouldn’t be better off than now. Explaining the distinction in terms specific to each type at the start of the page would be a horrible, mind-damaging thing that would make it much harder for people to actually understand (so please don’t “helpfully” do that). What’s needed is a simple yet basically correct summary of this information, but I’m pathologically incapable of writing it.
As a reward for reading all the way through this short and simplified summary, here’s a fun function you can copy straight into the console. It should work in any version of Oolite which actually has a console:
this.protoChain = function (object) { function pr(v) { // Get prototype of v, boxing it if it’s a primitive. if (typeof Object.getPrototypeOf == "function") return Object.getPrototypeOf(new Object(v)); else return v.__proto__; } var result = "", first = true; for (;;) { var proto = pr(object); if (!proto) return result; if (!first) result += ": "; else first = false; result += proto.constructor.name || "<anonymous>"; object = proto; } }
In trunk, protoChain(player.ship) returns “PlayerShip: Ship: Entity: Object”. In Oolite v.1.74, you get “Object: Object: Object: Object”, which is distinctly wrong albeit mildly amusing to fans of Catch-22. In either, protoChain(new Vector3D) returns “Vector3D: Object”, and protoChain([]) returns “Array: Object”. It also deals with (i.e., correctly lies about) primitive values; protoChain(5) returns “Number: Object”.
Note that Ahruman's masterpiece versions of Oolite came after this article - v.1.76.1 & v.1.77.1: the Article is Understanding JavaScript, maybe
See Also
- Oolite Javascript basics - 2022 essay by Massively Locked
- Mozilla JavaScript reference pages (check for JavaScript 6)
- Oolite JavaScript event handler reference
- Oolite JavaScript object model
- Variables in Oolite JavaScripts
- Javascript Operators
- Handling OXP Dependencies with JavaScript
- Javascript optimization tips
- Writing a debug message into the latest.log (2014)
- Linear algebra for game developers (First of 4 parts, recommended by Ahruman in 2010)
History
Updating Javascript/Spidermonkey
- Issues with updating (2021)