InkSpector 1.02 Scripts Copyright 2014 by Mark Incley Web and support via http://www.inkland.org.uk/ User's Manual Contents ======== 1.0 What Is An InkSpector Script? 2.0 Executing A Script 3.0 InkSpector Functions 4.0 Bitwise Functions 5.0 CTV Functions 6.0 CSpeccy Functions 7.0 Tips For Writing Scripts 1.0 What Is An InkSpector Script? ================================= A script is InkSpector's term for a Lua script file along with any Spectrum snapshot files that it may require. All of these files may be zipped up into a standard .zip file, although if there's no accompanying snapshot files, the script may be left as a stand-alone (uncompressed) .lua file. At the risk of labouring the point, a script can be: a) A zip file containing a .lua script file along with one or more Spectrum snapshots it may require. b) An uncompressed .lua file, such as Archive.lua supplied with InkSpector. c) An uncompressed .lua file along with one or more snapshots it may require. This option is only recommended for script development. Once the script has been developed, zip it up along with the snapshots, so that only one file needs to be distributed. Scripts are stored in the "Scripts" folder within the folder where InkSpector is installed. This is typically c:\Program Files\Inkland\InkSpector\Scripts All scripts must contain (if it's a .zip file) or be, a valid Lua file. For InkSpector to consider it valid, it must: 1. ...not contain any syntax errors! 2. ...define a table named "Script_Details". 3. ...define a function named "Script_Start". If the script isn't deemed as valid, it will be excluded by InkSpector and will not appear in the playlist editor. The contents of the Script_Details table are shown on the "Script Selection" dialog within InkSpector's configuration panel, as well as being used at various points in the InkSpector log. Script_Details table taken from archive.lua: Script_Details = { name = "Snapshot Archive", author = "Mark Incley", version = "1.0", description = "Randomly selects files from user's snapshot archive.", webpage = "www.inkland.org.uk", email = "inkspector@inkland.org", } 2.0 Executing A Script ====================== When a script is selected to run, InkSpector loads it up and attempts to call a function named Script_Start(). From this point on, it is up to the script to yield control back to InkSpector at regular, appropriate points - e.g. when you have created and/or moved your virtual Spectrums. The virtual Spectrums run independently of the script, so it's feasible to set up your Spectrums in script and then have the script exit, leaving the Spectrums running. To prevent scripts from being able to freeze InkSpector (by never yielding control), InkSpector will kill any script that doesn't yield within a generous, arbitrary period of Lua execution time. This time is independent of CPU speed the script is running on. You should never see any scripts being killed in this way, but if you do, the action is logged. Just as an example, here's a bit code that will get your script killed by InkSpector: -- Do a crazy amount of processing without yielding control for i=1,900000 do x = i * 123 end As well as being able to yield control back to InkSpector, a script can sleep for a period of time. This is exactly what the supplied Archive script does - it loads up a random snapshot then the script sleeps for a few seconds before loading another snapshot. When a script has run for the amount of time specified on the InkSpector control panel, it will kill your script and load up the next one. This is all done transparently to the script and doesn't have to be taken into consideration when writing one. 3.0 InkSpector Functions ======================== All InkSpector functions are contained within the inks table. inks.SetBackgroundColour(r, g, b) Sets the background colour where the red,green,blue components are in the range 0-255 Example: inks.SetBackgroundColour(math.rand(32,128), math.rand(32,128), math.rand(32,128)) inks.GetArchiveFolder() Returns the name of the archive folder set up in the Inkspector control panel. If the folder hasn't been set up, this function will return an empty string. inks.GetTimer() Returns the absolute system time. Example: startTime = inks.GetTimer() elapsed = inks.GetTimer() - startTime inks.BuildArchiveList() Recursively scans the InkSpector snapshot archive folder (typically C:\Program Files\Inkland\InkSpector\Snapshots) in addition to any configured in the InkSpector screensaver control panel, to build the list of files that are returned by inks.SelectFileFromArchive() Returns the number of snapshot files found. Subsequent calls to inks.BuildArchiveList() in the same session will not cause the snapshot folder(s) to be re-scanned. The function will return immediately with the number of snapshot files found the first time this function was called. This function is called implicitly by inks.SelectFileFromArchive() and inks.FindFileInArchive() Example: if inks.BuildArchiveList() == 0 then inks.NextScript(true) end inks.SelectFileFromArchive([n]) Returns the fully qualified filename of a random (when n is not supplied) or the nth snapshot within the snapshot folder as specified by the user in the InkSpector Control Panel. If n is out of range, this function will return nil. inks.BuildArchiveList() is called implicitly by this function. Example: snapshot = inks.SelectFileFromArchive() -- pick a random file from the archive snapshot4 = inks.SelectFileFromArchive(3) -- pick the 4th (n is zero-based) file in the archive If SmartPlay is enabled and this function is called without any parameters (i.e. pick a random snapshot), it will select a file is randomly from the archive without repeating any of those that have already been selected. InkSpector writes SmartPlay's state to \Inkland\SmartPlay.dat when it exits, to ensure continuity. inks.FindFileInArchive(filename) Searches the InkSpector and user's snapshot folders for a specific filename and returns the fully qualified filename of the first matching file. If the filename isn't found, this function returns nil. inks.BuildArchiveList() is called implicitly by this function. Example: -- See if we've got Monty recording available somwhere in our archive snapshot = inks.FindFileInArchive("monty.rzx") if snapshot then speccy:LoadSnapshot(snapshot) end inks.NextScript(omitThisSession) Instructs InkSpector to load the next script in the playlist. If omitThisSession is "true", the script is omitted from the playlist for the current session. If omitThisSession is "false", the script will be allowed to run from the playlist when its time comes around again. This function doesn't return a value. Example: inks.NextScript(true) inks.Sleep([time]) Yields control back to Inkspector and causes the script to sleep for a specified number of seconds. Spectrums will continue to be emulated and displayed, even if the script is sleeping. Example: inks.Sleep() -- Give control back to InkSpector, but don't sleep. inks.Sleep(10.5) -- Give control back to InkSpector and sleep for 10.5 seconds inks.SetCameraPos(x, y, z) Sets the camera's world position. Doesn't return a value. Example: inks.SetCameraPos(100, 200, 300) inks.SetCameraLookAt(x, y, z) Sets the world position where the camera should look at. Doesn't return a value. Example: inks.SetCameraLookAt(0, 0, 200) inks.DestroySpeccies() Destroys all Speccies and clones. Doesn't return a value. Example: inks.DestroySpeccies() inks.Text(x ,y, message [,time]) Displays the message at coordinates x, y, optionally for a specified number of seconds. Omitting the time parameter is the same as specifying a time value of 0, which causes the message to be printed once only - it will not persist for any time. A unique message id is returned which can be used to call inks.IsTextActive to check whether it's still on screen. By design, this function will not display text when running in screensaver preview mode and will return an id of 0. inks.IsTextActive(id) Returns true if the text message with the id returned by inks.Text() is still on screen. inks.ClearAllText() Removes all text messages from the screen Doesn't return a value. inks.Random([upper] | [lower, upper]) This function is a clone of Lua's own math.random() except that it uses a better random number generator than the standard C library one that Lua uses. inks.Random() with no arguments generates a real number between 0 and 1 inks.Random(upper) generates integer numbers between 1 and upper inks.Random(lower, upper) generates integer numbers between lower and upper inks.GetTimer() Returns the current system time in seconds. Example: if inks.GetTime() > (startTime + 5.0) then DoSomethingElse() end inks.GetSnapshotsFolder() Returns the fully qualified path of Inkspector's own snapshot archive (not the user-specified one on the control panel). Typically this is %ProgramFiles%\Inkland\InkSpector\Snapshots Example (from "Miner Willy.lua" script): jsw2 = CSpeccy48:new(inks.GetSnapshotsFolder().."\\rzx\\manic.rzx") This function is available in InkSpector 1.01 and later only. 4.0 Bitwise Functions ===================== Lua doesn't supply any bitwise operators out of the box, so these functions supplied by InkSpector in the "bit" table may be used: bit.shiftr(value, n) Returns value shifted right by n bits. Example: idivBy4 = bit.shiftr(128, 2) bit.shiftl(value, n) Returns value shifted left by n bits. Example: imulBy8 = bit.shiftl(32, 3) bit.test(value, bitnum) Returns true if the bitnum of value is set to 1, otherwise returns false. bit.reset(value, bitnum) Returns value with bitnum set to 0. Example: makeEven = bit.reset(3, 0) bit.set(value, bitnum) Returns value with bitnum set to 1. Example: makeOdd = bit.set(2, 0) bit.bnot(num) Returns the inverted value of num Example: inverted = bit.bnot(12345) bit.bxor(num, xorvalue) Returns num XOR'd with xorvalue Example: chequered = bit.bxor(128, 77) bit.bor(num, orvalue) Returns num OR'd with orvalue Example: ord = bit.bor(64, 128) bit.band(num, andvalue) Returns num ANDed with andvalue Example: anded = bit.band(129, 128) The reason for using "band", "bor" and "bnot" is because the words "and", "or" and "not" are reserved by Lua. 5.0 CTV Functions ================= CTV is the basic TV object. It doesn't contain an emulated Spectrum - just a static display. It may be manipulated with the following functions: CTV:new() Create a new TV Returns a CTV Example: myTV = CTV:new() CTV:SetWorldPos(x, y, z) Sets the TV's world position to x, y, z Doesn't return a value Example: myTV = CTV:new() myTV:SetWorldPos(123.4, 567.8, 901.2) CTV:SetWorldRot(yaw, pitch, roll) Sets the rotation of the TV Example: myTV = CTV:new() myTV:SetWorldRot(0, 0, math.pi / 2.0) CTV:Show(true | false) Shows (if true) or hides (if false) the TV Example: myTV = CTV:new() myTV:Show(true) CTV:SetExclusive(true | false) If true, sets the CTV to be rendered exclusively, without its TV model surround with the display completely filling the screen. If false, all CTVs are rendered in the 3D world. Note that any CTV can take away the exclusive mode, not just the CTV that set the exclusive mode in the first instance. A CTV in exclusive mode that is deleted will automatically set NonExclusive mode to render any remaining CTVs. CTV::IsExclusive() Returns whether the CTV is the exclusive CTV as set by a call to SetExclusive() above. 6.0 CSpeccy Functions ===================== There are currently two flavours of CSpeccys: CSpeccy48 for 16/48K models and CSpeccy128 for the 128K/+2 models. Unless otherwise noted, all CSpeccy48 functions apply to CSpeccy128s too. CSpeccy48 is derived from CTV, and therefore includes all of the functionality of CTV above. In addition, it contains a virtual Spectrum and the following functions: CSpeccy48:new() Create a new CSpeccy48 which can be thought of as a CTV along with the virtual Spectrum inside. Returns a CSpeccy48 Example: mySpeccy = CSpeccy48:new() CSpeccy48:IsPaused() Returns true if the Spectrum emulation is currently paused, otherwise false. Example: if mySpeccy:IsPaused() then DoSomethingWhilePaused() end CSpeccy48:Pause(true | false) Pauses (true) or unpauses (false) the emulation of the Spectrum Returns nothing Example: mySpeccy:Pause(true) -- pause the Spectrum mySpeccy:Pause(false) -- unpause the Spectrum This function is called implicitly with "true" when loading a .scr file. Note that this function pauses only the Z80 emulation. The ULA continues to be emulated to allow for .scr screenshots to show flashing attributes. CSpeccy48:GetSnapshotCreator() Returns the name of the program that created the snapshot that has been loaded. If the snapshot format doesn't support such information, this function will return nil. Currently .szx and .rzx snapshot formats support such information. CSpeccy48:IsPlayingRZX() Returns true if the Spectrum is currently playing a RZX (replay) snapshot, otherwise false. Example: while speccy:IsPlayingRZX() do inks.Sleep() end CSpeccy48:StopRZXPlayback() Stops playback of an RZX file when in progress Example: if speccy:IsPlayingRZX() then speccy:StopRZXPlayBack() -- Bored of this now... end CSpeccy48:RZXCompleted() Returns true if the last RZX input played up until the end of the input stream (i.e. it wasn't interrupted or lost sync, etc) Example: while speccy:IsPlayingRZX() do inks.Sleep() end if not speccy:RZXCompleted() then inks.Log("RZX failed to play completely") end CSpeccy48:Reset() Reset the Spectrum Returns nothing. Example: mySpeccy:Reset() CSpeccy48:Clone(x, y, z) Creates a clone CTV of a CSpeccy48 at world position x, y, z. Clones look exactly like their parents, but don't have the processing overhead of the Spectrum emulation (they simply take a copy of their parent's display). As clones are CTVs they can be manipulated by the CTV functions in section 5.0 Returns a CTV clone. Example: myClone = mySpeccy:Clone(100, 200, 300) myClone:SetWorldRot(0, math.pi / 2, 0) CSpeccy48:Peek(addr) Returns the 8-bit value of memory at address addr (0 <= addr < 65536) For a CSpeccy128, the currently selected memory pages are taken into consideration. Example: mem = mySpeccy:Peek(16384) CSpeccy48:Poke(addr, newval) Changes memory at address addr to hold the value newval. Returns the previous value at the address. For a CSpeccy128, the currently selected memory pages are taken into consideration. If an attempt is made to poke the ROM, it is ignored and a Peek(addr) is performed instead. Example: prevVal = mySpeccy:Poke(22528, 123) CSpeccy48:GetBorder() Returns the current colour of the CSpeccy48's border (0-7) Example: border = mySpeccy:GetBorder() CSpeccy48:LoadSnapshot(snapshotname) Loads a snapshot into the CSpeccy48. InkSpector currently supports .sna, .z80, .scr (screen dumps), .szx, .rom (Interface 2 ROMs) and .rzx replay snapshot formats. If snapshotname contains a fully qualified path (such as that returned by inks.SelectFileFromArchive()) it is loaded from that location. Othwerwise, the snapshot is assumed to live either in the same .zip file as the current script (if the current script is inside a zip file) or in the InkSpector\Scripts folder if the current script is a .lua file (i.e. not inside a zip file) Returns 0 if the snapshot was loaded successfully. Reasons for a snapshot failing to load could include: * snapshot file damaged * snapshot file is for hardware not yet supported by InkSpector (e.g. a Spectrum with Interface 1 attached) If a snapshot saved out with 16k or 48k hardware is loaded into a CSpeccy128, InkSpector pages in the original Spectrum 16/48K ROM (this is not the same as the 128's "48k" ROM) into the machine to ensure it will load OK. Without this behaviour, 48K RZX files could lose sync quickly as the modified 48K ROM in the 128K model performs extra "in" instructions - presumably to scan for the 128K's optional number keypad in 48K mode. If a .scr snapshot is loaded, the CSpeccy is paused (see CSpeccy48:Pause) to allow the picture to be displayed without disturbance. Loading any other type of snapshot will cause the CSpeccy to be unpaused. Also note that loading .scr snapshot is slightly different from loading other supported snapshot formats in that, display aside, it leaves the state of the CSpeccy untouched. CSpeccy48:Mute(true | false) Mute the Spectrum Example: mySpeccy:Mute(true) -- Mute the Speccy mySpeccy:Mute(false) -- Un-mute the Speccy If the "Emulate Beeper" or "Emulate AY" options are unchecked on the InkSpector control panel, un-muting will not result in that sound being heard. CSpeccy48:IsMuted() Returns true if the CSpeccy48 is currently muted, otherwise false. Example: if not mySpeccy:IsMuted() then mySpeccy:SetVolume(50) end CSpeccy48:SetVolume(vol) Sets the Spectrum's beeper and AY chip volume, where volume is in the range 0-100. Note that setting volume to 0 (silent) is not the same as muting it. It's possible for the volume to be 0 and IsMuted() to return false. Example: mySpeccy:SetVolume(50) CSpeccy48:GetVolume() Returns the Spectrum's beeper volume, which will be in the range 0-100. Example: vol = mySpeccy:GetVolume() 7.0 Tips For Writing Scripts ============================ Visit www.lua.org and download the complete Lua reference manual from there for documentation on the features and functions available to Lua in addition to the InkSpector extensions: http://www.lua.org/ftp/refman-5.0.pdf Visit http://www.scintilla.org/SciTE.html and download the Windows version of SciTE. It is an excellent freeware editor that supports Lua (along with many other common languages). I used SciTE to write the InkSpector scripts, since it's possible to hit F5 within SciTE to run the script in Lua. Of course, this won't allow you to see the script in action, but it will catch any syntax errors in your Lua script before you hand it over to InkSpector. This is how I wrote the sample InkSpector scripts. Lua is a case-sensitive language. Make sure the exact function names as shown above are used. Beware of the following, from the Lua 5.0 reference manual: "Both false and nil are considered false. All values different from nil and false are considered true (in particular, the number 0 and the empty string are also true)." i.e. "if 0 then end" *will* cause to be executed since 0 is different from nil and false. Grab one of the scripts supplied with InkSpector and hack it beyond recognition! :-)