Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions docs/formats/mission-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,14 +339,35 @@ trigger's condition:
The reference lists each condition's values
([Conditions](../guide/mission-reference.md#conditions)).

### Edits

An editor that changes records through forms, such as the
[mission editor](https://github.com/OpenReliant/openreliant-editor), edits the source with
[`edit.zig`](../../src/formats/dte/source/edit.zig) instead of writing the whole file again, so the
rest of the text keeps its comments and layout. Each edit replaces one span of the text, the way an
undoable text editor replaces it:

- `setArgument` sets an argument of a declaration, such as a ship's display name.
- `setField` sets a field of a declaration's table. It replaces the value if the table has the
field, and otherwise adds the field after the last item: on a new line with the same indent if
the table spans several lines, and on the same line if not. A call without a table gets one,
before a trigger's function.
- `removeField` removes a field. A field alone on its line goes with the line and its comment.
- `declare` adds a declaration on a new line, such as after the last one with the same word.

Values are written the way the decompiler writes them. `language.fieldsOf` gives the table of
fields that each declaration takes and which argument it is, and the table gives each field's type
and description for a form.

## Reading the text

Luau's own parser reads mission source, so the syntax is exactly Luau's, comments included.
[`src/source_parser.zig`](../../src/source_parser.zig) calls the parser through a C++ shim, since
it has no C API, and copies its syntax tree into the library's
the parser has no C API, and copies its syntax tree into the library's
([`syntax.zig`](../../src/formats/dte/source/syntax.zig)): each statement and expression with the
line and column it starts at, and each error the parser finds with its place. Only the tools that
read source link the parser. sltool does; the library the tools share doesn't.
line and column where it starts and ends, and each error the parser finds with its place. Columns
count bytes, as in Luau. A table's item starts at its key and ends with its value. Only the tools
that read source link the parser: sltool does, and the library that the tools share doesn't.

Mission source uses a subset of Luau. The parser gives back the rest, such as a loop, a function
declared by name, a method call, an `if` expression or a string with values put into it, as an
Expand Down
5 changes: 3 additions & 2 deletions src/engine/game/mission/events.zig
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,9 @@ fn firstArmed(list: []const Watch, object: u16) ?Watch {
/// How far CloseProximity's watches look, in the subject's radii (`0x004DC72C`).
const close_reach: f32 = 20;

/// How far ShipReached's watches look (`0x4B742400`, its square).
const reached_reach: f32 = 4000;
/// How far ShipReached's watches look (`0x4B742400`, its square), which an editor's map draws
/// around a waypoint or a nav point.
pub const reached_reach: f32 = 4000;

/// The operand of a Proximity trigger that gives its distance, in the subject's radii.
const proximity_operand = 1;
Expand Down
2 changes: 2 additions & 0 deletions src/formats/dte/source.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub const write = @import("source/write.zig");
pub const syntax = @import("source/syntax.zig");
/// The text compiled into a mission file.
pub const compile = @import("source/compile.zig");
/// Edits to the text as an editor makes them.
pub const edit = @import("source/edit.zig");
/// What the source's words take and give, and the fields of its tables.
pub const language = @import("source/language.zig");
/// The definitions file for luau-lsp.
Expand Down
367 changes: 367 additions & 0 deletions src/formats/dte/source/edit.zig

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions src/formats/dte/source/language.zig
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,28 @@ pub fn declaration(word: Word) Declaration {
};
}

/// The table of fields that a declaration takes, and which of its arguments it is.
pub const FieldsArgument = struct {
index: usize,
table: Table,
};

/// The table of fields that `word` takes, if it takes one: what an editor's form for the record
/// shows, and where its edits go.
pub fn fieldsOf(word: Word) ?FieldsArgument {
return switch (word) {
.mission => .{ .index = 0, .table = mission_fields },
.flight_group => .{ .index = 1, .table = flight_group_fields },
.ship => .{ .index = 1, .table = ship_fields },
.nav_point, .waypoint, .curve_point => .{ .index = 1, .table = marker_fields },
.point => .{ .index = 1, .table = point_fields },
.curve => .{ .index = 0, .table = curve_fields },
.part => .{ .index = 2, .table = part_fields },
.on => .{ .index = 2, .table = trigger_fields },
else => null,
};
}

/// `word`'s signature, as Luau writes it: `ship(name: string, fields: ShipFields): MissionShip`,
/// or `any_ship: MissionShip` for a value.
pub const Signature = struct {
Expand Down Expand Up @@ -375,6 +397,24 @@ test parameters {
try std.testing.expectEqualStrings("entity_to_be_controlled: Entity; ai_mode: Order; initialize_immediately: number; entity_to_target: Entity?; ", w.buffered());
}

test fieldsOf {
// A word has a table exactly if it has a `fields` parameter: that parameter, of the table's
// type.
for (std.enums.values(Word)) |word| {
var listed = std.mem.splitSequence(u8, declaration(word).parameters orelse "", ", ");
var index: usize = 0;
const parameter = while (listed.next()) |parameter| : (index += 1) {
if (std.mem.startsWith(u8, parameter, "fields: ")) break parameter;
} else {
try std.testing.expect(fieldsOf(word) == null);
continue;
};
const fields = fieldsOf(word).?;
try std.testing.expectEqual(index, fields.index);
try std.testing.expect(std.mem.find(u8, parameter, fields.table.name) != null);
}
}

test Signature {
var buffer: [256]u8 = undefined;
var w: Writer = .fixed(&buffer);
Expand Down
47 changes: 28 additions & 19 deletions src/formats/dte/source/syntax.zig
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! Mission source as Luau's parser reads it
//! ([Mission source](../../../../docs/formats/mission-source.md)): a tree of the statements and
//! expressions in the file, each node with the line and column it starts at. The `source_parser`
//! module fills it with Luau's own parser, which only the tools that read source link. A construct
//! mission source doesn't use, such as a loop, is an `unsupported` node, so that the compiler can
//! say where it is.
//! expressions in the file, each node with the line and column where it starts and ends. The
//! `source_parser` module fills it with Luau's own parser, which only the tools that read source
//! link. A construct that mission source doesn't use, such as a loop, is an `unsupported` node, so
//! that the compiler can say where it is.

const std = @import("std");

Expand Down Expand Up @@ -106,7 +106,8 @@ pub const BinaryOperator = enum(u8) {
/// The forms of a table's item: `value`, `key = value` and `[key] = value`.
pub const ItemKind = enum(u8) { list, record, general };

/// Where a node or an error starts, counting lines and columns from 0, as Luau does.
/// A place in the source, where a node or an error starts or a node ends. Lines and columns count
/// from 0, and columns count bytes, as in Luau.
pub const Position = extern struct {
line: u32,
column: u32,
Expand All @@ -125,6 +126,8 @@ pub const Node = extern struct {
/// an assignment's or a function's children are its names, its targets or its parameters.
op: u8,
at: Position,
/// Just past the node's last byte.
end: Position,
/// The node's children, `count` of them from `first` in `Tree.children`.
first: u32,
count: u32,
Expand Down Expand Up @@ -291,6 +294,11 @@ pub const Tree = struct {
return tree.nodes[index].at;
}

/// Where node `index` ends, just past its last byte. A table's item ends with its value.
pub fn end(tree: Tree, index: Index) Position {
return tree.nodes[index].end;
}

fn childrenOf(tree: Tree, index: Index) []const Index {
const node = tree.nodes[index];
return tree.children[node.first..][0..node.count];
Expand Down Expand Up @@ -328,19 +336,19 @@ pub const Tree = struct {
test Tree {
// `x += -2` and `on("a", { b = true })`, built by hand as the bridge writes them.
const nodes = [_]Node{
.{ .kind = .global_name, .op = 0, .at = .{ .line = 0, .column = 0 }, .first = 0, .count = 0, .text_start = 0, .text_length = 1, .number = 0 },
.{ .kind = .number, .op = 0, .at = .{ .line = 0, .column = 6 }, .first = 0, .count = 0, .text_start = 1, .text_length = 0, .number = 2 },
.{ .kind = .unary, .op = @backingInt(UnaryOperator.minus), .at = .{ .line = 0, .column = 5 }, .first = 0, .count = 1, .text_start = 1, .text_length = 0, .number = 0 },
.{ .kind = .compound_assign, .op = @backingInt(BinaryOperator.add), .at = .{ .line = 0, .column = 0 }, .first = 1, .count = 2, .text_start = 1, .text_length = 0, .number = 0 },
.{ .kind = .global_name, .op = 0, .at = .{ .line = 1, .column = 0 }, .first = 3, .count = 0, .text_start = 1, .text_length = 2, .number = 0 },
.{ .kind = .string, .op = 0, .at = .{ .line = 1, .column = 3 }, .first = 3, .count = 0, .text_start = 3, .text_length = 1, .number = 0 },
.{ .kind = .string, .op = 0, .at = .{ .line = 1, .column = 10 }, .first = 3, .count = 0, .text_start = 4, .text_length = 1, .number = 0 },
.{ .kind = .boolean, .op = 1, .at = .{ .line = 1, .column = 14 }, .first = 3, .count = 0, .text_start = 5, .text_length = 0, .number = 0 },
.{ .kind = .item, .op = @backingInt(ItemKind.record), .at = .{ .line = 1, .column = 10 }, .first = 3, .count = 2, .text_start = 5, .text_length = 0, .number = 0 },
.{ .kind = .table, .op = 0, .at = .{ .line = 1, .column = 8 }, .first = 5, .count = 1, .text_start = 5, .text_length = 0, .number = 0 },
.{ .kind = .call, .op = 0, .at = .{ .line = 1, .column = 0 }, .first = 6, .count = 3, .text_start = 5, .text_length = 0, .number = 0 },
.{ .kind = .call_statement, .op = 0, .at = .{ .line = 1, .column = 0 }, .first = 9, .count = 1, .text_start = 5, .text_length = 0, .number = 0 },
.{ .kind = .block, .op = 0, .at = .{ .line = 0, .column = 0 }, .first = 10, .count = 2, .text_start = 5, .text_length = 0, .number = 0 },
.{ .kind = .global_name, .op = 0, .at = .{ .line = 0, .column = 0 }, .end = .{ .line = 0, .column = 1 }, .first = 0, .count = 0, .text_start = 0, .text_length = 1, .number = 0 },
.{ .kind = .number, .op = 0, .at = .{ .line = 0, .column = 6 }, .end = .{ .line = 0, .column = 7 }, .first = 0, .count = 0, .text_start = 1, .text_length = 0, .number = 2 },
.{ .kind = .unary, .op = @backingInt(UnaryOperator.minus), .at = .{ .line = 0, .column = 5 }, .end = .{ .line = 0, .column = 7 }, .first = 0, .count = 1, .text_start = 1, .text_length = 0, .number = 0 },
.{ .kind = .compound_assign, .op = @backingInt(BinaryOperator.add), .at = .{ .line = 0, .column = 0 }, .end = .{ .line = 0, .column = 7 }, .first = 1, .count = 2, .text_start = 1, .text_length = 0, .number = 0 },
.{ .kind = .global_name, .op = 0, .at = .{ .line = 1, .column = 0 }, .end = .{ .line = 1, .column = 2 }, .first = 3, .count = 0, .text_start = 1, .text_length = 2, .number = 0 },
.{ .kind = .string, .op = 0, .at = .{ .line = 1, .column = 3 }, .end = .{ .line = 1, .column = 6 }, .first = 3, .count = 0, .text_start = 3, .text_length = 1, .number = 0 },
.{ .kind = .string, .op = 0, .at = .{ .line = 1, .column = 10 }, .end = .{ .line = 1, .column = 11 }, .first = 3, .count = 0, .text_start = 4, .text_length = 1, .number = 0 },
.{ .kind = .boolean, .op = 1, .at = .{ .line = 1, .column = 14 }, .end = .{ .line = 1, .column = 18 }, .first = 3, .count = 0, .text_start = 5, .text_length = 0, .number = 0 },
.{ .kind = .item, .op = @backingInt(ItemKind.record), .at = .{ .line = 1, .column = 10 }, .end = .{ .line = 1, .column = 18 }, .first = 3, .count = 2, .text_start = 5, .text_length = 0, .number = 0 },
.{ .kind = .table, .op = 0, .at = .{ .line = 1, .column = 8 }, .end = .{ .line = 1, .column = 20 }, .first = 5, .count = 1, .text_start = 5, .text_length = 0, .number = 0 },
.{ .kind = .call, .op = 0, .at = .{ .line = 1, .column = 0 }, .end = .{ .line = 1, .column = 21 }, .first = 6, .count = 3, .text_start = 5, .text_length = 0, .number = 0 },
.{ .kind = .call_statement, .op = 0, .at = .{ .line = 1, .column = 0 }, .end = .{ .line = 1, .column = 21 }, .first = 9, .count = 1, .text_start = 5, .text_length = 0, .number = 0 },
.{ .kind = .block, .op = 0, .at = .{ .line = 0, .column = 0 }, .end = .{ .line = 1, .column = 21 }, .first = 10, .count = 2, .text_start = 5, .text_length = 0, .number = 0 },
};
const children = [_]Index{ 1, 0, 2, 6, 7, 8, 4, 5, 9, 10, 3, 11 };
const tree: Tree = .{ .nodes = &nodes, .children = &children, .text = "xonab", .errors = &.{}, .root = 12 };
Expand All @@ -352,8 +360,9 @@ test Tree {
const item = tree.get(8).item.record;
try std.testing.expectEqualStrings("b", item.key);
try std.testing.expect(tree.get(item.value).boolean);
// An item starts at its key.
// An item starts at its key, and ends with its value.
try std.testing.expectEqual(Position{ .line = 1, .column = 10 }, tree.at(8));
try std.testing.expectEqual(Position{ .line = 1, .column = 18 }, tree.end(8));

var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer out.deinit();
Expand Down
12 changes: 6 additions & 6 deletions src/formats/dte/source/write.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub fn write(writer: *Writer, arena: Allocator, mission: dte.Mission) Error!void
}

/// The longest line a declaration's fields keep to before they go on lines of their own.
const line_length = 100;
pub const line_length = 100;

const Source = struct {
arena: Allocator,
Expand Down Expand Up @@ -457,7 +457,7 @@ fn writeList(w: *Writer, items: []const []const u8, column: usize) Writer.Error!

/// `items` as a table, `{ a, b }`, starting `column` characters into its line, with `after`
/// characters after it on the line: on that line where it fits, or with an item to a line.
fn writeTable(w: *Writer, items: []const []const u8, column: usize, after: usize) Writer.Error!void {
pub fn writeTable(w: *Writer, items: []const []const u8, column: usize, after: usize) Writer.Error!void {
if (items.len == 0) return w.writeAll("{}");
var length = column + "{ }".len + after;
for (items, 0..) |item, index| length += item.len + @as(usize, if (index > 0) ", ".len else 0);
Expand Down Expand Up @@ -488,7 +488,7 @@ fn named(kind: vocabulary.Kind, value: u32) Named {

/// A number as Luau writes it: the shortest that reads back as the same `f32`, with an exponent
/// from `exponent_from` on, and `0/0`, `1/0` and `-1/0` for those Luau has no literal for.
const Number = struct {
pub const Number = struct {
value: f32,

/// The smallest number written with an exponent, 2^53. From there on a double holds only some
Expand All @@ -503,20 +503,20 @@ const Number = struct {
}
};

fn number(value: f32) Number {
pub fn number(value: f32) Number {
return .{ .value = value };
}

/// A place as `{ x, y, z }`.
const Vector = struct {
pub const Vector = struct {
value: [3]f32,

pub fn format(shown: Vector, w: *Writer) Writer.Error!void {
try w.print("{{ {f}, {f}, {f} }}", .{ number(shown.value[0]), number(shown.value[1]), number(shown.value[2]) });
}
};

fn vector(value: [3]f32) Vector {
pub fn vector(value: [3]f32) Vector {
return .{ .value = value };
}

Expand Down
Loading
Loading