ftlman scriptable Lua patching API
Since v0.5 ftlman supports running Lua scripts during patching and provides a Lua API that allows you to programatically change files in the FTL data archive.
Lua append scripts
Lua append scripts are lua equivalents of .append.xml
files.
They have to be named <some existing xml file without .xml>.append.lua
and will allow you to
modify the existing xml file whose file stem you substituted.
These scripts will have an additional global defined at runtime called document
.
This global will be of type Document, described below.
class Document { readonly .root: Element; }
Most important of all, document
lets you access and modify the DOM tree of the XML file
by accessing the .root
property.
All modifications done to the DOM will be written to the existing file after
the script finishes running.
Technical remarks
- These scripts will be executed alongside
.append.xml
files, and the order in which they run is unspecified. - Note that as with normal
.append.xml
files, these won’t be executed if the base XML file doesn’t exist.
Script environment
Apart from the potential document
global, the mod
global will always be present
in the script’s environment.
It serves as the bridge between the mod manager and your scripts. The API
currently exposed by this global is described under the various “library” sections below.
DOM library
type NodeType = ("element" | "text");
class Node { readonly .type: NodeType; /// Previous sibling node of this node. readonly .previousSibling: Node?; /// Next sibling node of this node. readonly .nextSibling: Node?; /// Parent node of this node. readonly .parent: Element?; /// Attempts to cast this `Node` as an `Element`. Returns `nil` if this is node is not an `Element`. fn :as(type: "element") -> Element?; /// Attempts to cast this `Node` as a `Text` node. Returns `nil` if this is node is not a `Text` node. fn :as(type: "text") -> Text?; /// Inserts the specified nodes into the child list of this node's parent directly before this Node. /// String values are implicitly converted into text nodes. fn :before(...: (Node | string)) -> nil; /// Inserts the specified nodes into the child list of this node's parent directly after this Node. /// String values are implicitly converted into text nodes. fn :after(...: (Node | string)) -> nil; /// Removes the Node from its parent's child list. fn :detach() -> nil; }
class Text: Node { readonly .type: "text"; /// Text content of this text node. .content: string; }
class Element: Node { readonly .type: "element"; /// Name component of this node's XML tag. .name: string; /// Prefix component of this node's XML tag. .prefix: string; /// First child of this element that is also an element. readonly .firstElementChild: Element?; /// Last child of this element that is also an element. readonly .lastElementChild: Element?; /// First child node of this element. readonly .firstChild: Node?; /// Last child node of this element. readonly .lastChild: Node?; /// Contents of all the text nodes in this element's subtree concatenated together. .textContent: string; /// Proxy object that allows inspecting and modifying element attributes. Attribute values can be accessed and set as fields of this object. /// Can also act as a method, in which case it returns an iterator over all attributes of the element. /// Values will be parsed as either a number or boolean and the parsed value will be returned on success, otherwise the original string value is returned. For a variant that does not perform this parsing see `rawattrs` below. /// Note that only the field itself is read only, assigning to individual attributes will work. readonly .attrs: ElementAttributes; /// Similar to `attrs` but does not attempt to parse attribute values and will always return a string. /// Only strings can be assigned as values. readonly .rawattrs: RawElementAttributes; /// Returns an iterator over all immediate Element children of this Element. fn :children() -> fn() -> Element?; /// Returns an iterator over all immediate children of this Element. fn :childNodes() -> fn() -> Node?; /// Appends the specified nodes to the end of this Element's child list. /// String values are implicitly converted into text nodes. fn :append(...: (Node | string)) -> nil; /// Prepends the specified nodes to the end of this Element's child list. /// String values are implicitly converted into text nodes. fn :prepend(...: (Node | string)) -> nil; }
type AttributeValue = (string | number | bool);
class ElementAttributes { operator __call(element_receiver: Element) -> fn() -> (string, AttributeValue)?; operator __index(name: string) -> AttributeValue?; operator __newindex(name: string, value: AttributeValue?) -> nil; }
class RawElementAttributes { operator __call(element_receiver: Element) -> fn() -> (string, string)?; operator __index(name: string) -> string; operator __newindex(name: string, value: string) -> nil; }
/// Create a new `Element` with the specified prefixed name and attributes. fn mod.xml.element(prefix: string, name: string, attrs: Table<string, string>?) -> Element;
/// Create a new `Element` with the specified name and attributes. fn mod.xml.element(name: string, attrs: Table<string, string>?) -> Element;
/// Parse `xml` as an FTL-style XML document. /// Returns all root nodes of the XML document. If present, <FTL> root tags will be stripped and all elements inside will be treated as if they were root nodes. fn mod.xml.parse(xml: string) -> Node...;
/// Serialize nodes as an XML document. fn mod.xml.stringify(...: Node) -> string;
Utility library
/// Returns `table` with its metatable replaced by one that disallows changing it. fn mod.util.readonly<T>(table: T) -> T;
/// Evaluates Lua code in `code` as an expression if possible, otherwise runs it as a block and returns the result. /// The code is run in the environment provided by the mandatory `env` option. The `name` option can be supplied to change the name of the chunk as seen in error messages. /// Currently only available on ftlman master. fn mod.util.eval<T>(code: string, options: Table?) -> any...;
Iterator library
/// Counts the number of elements returned by `iterator`. fn mod.iter.count<T>(iterator: fn() -> T?) -> integer;
/// Returns a new iterator that, for each value returned by `iterator`, returns the result of passing it to `mapper`. Iteration stops when either `iterator` or `mapper` first return `nil`. fn mod.iter.map<T, U>(iterator: fn() -> T?, mapper: fn(T) -> U?) -> U?;
/// Returns all values returned by `iterator` as an array. fn mod.iter.collect<T>(iterator: fn() -> T?) -> T[];
/// Returns a new iterator that, for each value returned by `iterator`, returns its index counting from `start` along with the value. /// If `start` is `nil` then `1` is used as the initial index fn mod.iter.enumerate<T>(iterator: fn() -> ...T?, start: integer?) -> fn() -> (integer, ...T)?;
fn mod.iter.zip<T, U>(a: fn() -> ...T?, b: fn() -> ...U?) -> fn() -> (...T, ...U)?;
Table library
/// Returns a new iterator over the array part of table `array`. fn mod.table.iter_array<T>(array: T[]) -> fn() -> T?;
/// Lexicographically compares the elements of arrays `a` and `b`. /// If the arrays are equal returns `0`. If array `a` is lexicographically smaller than `b` returns a negative number that is the negation of the position where the first mismatch occured. If array `a` is lexicographically greater than `b` returns the position where the mismatch occurred. fn mod.table.compare_arrays<T>(a: T[], b: T[]) -> integer;
Debugging library
/// Formats `value` in an unspecified human readable format according to the options in `options`. /// Returns the result as a string. The exact contents of this string must not be relied upon and may change. fn mod.debug.pretty_string(value: any, options: Table?) -> string;
/// Prints `value` in an unspecified human readable format according to the options in `options`. fn mod.debug.pretty_print(value: any, options: Table?) -> nil;
/// Raises an error if `a` is not equal to `b` according to an unspecified deep equality relation. fn mod.debug.assert_equal(a: any, b: any) -> nil;
Virtual filesystem library
type FileType = ("file" | "dir" | "other");
/// Information about a single file, returned by `Vfs:stat`. /// Unlike most classes here, this is a real table instead of a userdata. class FileMetadata { .type: FileType; /// If `kind` is "file", the length of the file in bytes and `nil` otherwise. .length: integer?; }
/// A single directory entry, returned by `Vfs:ls`. /// Unlike most classes here, this is a real table instead of a userdata. class DirectoryEntry { .type: FileType; .filename: string; }
/// A virtual filesystem. /// You can obtain a handle to a virtual filesystem by indexing the `mod.vfs` global with the filesystem's name. For example `mod.vfs.pkg` will give you a handle to the filesystem of the FTL data archive (ftl.dat) currently being patched. class Vfs { fn :stat(path: string) -> FileMetadata; fn :ls(path: string) -> DirectoryEntry[]; fn :read(path: string) -> string; fn :write(path: string, content: string) -> nil; }