diff --git a/docs/guide/openreliant.d.luau b/docs/guide/openreliant.d.luau index ad8a0d27..fa9891fb 100644 --- a/docs/guide/openreliant.d.luau +++ b/docs/guide/openreliant.d.luau @@ -880,6 +880,8 @@ export type Hud = { register_display: (name: string, definition: {frame: (seconds: number) -> (), replaces: { HudInstrument }?, layout: { [HudInstrument]: HudLayout }?, parts: { [HudPart]: HudPartLayout }?}) -> string, --- Turns the display `name` on or off: the calling mod's by its own name, or any mod's by the qualified one. Returns whether it's registered. set_display_enabled: (name: string, enabled: boolean) -> boolean, + --- Changes where the display `name` puts the game's instruments and their parts, after it registers: the calling mod's by its own name, or any mod's by the qualified one. `layout` and `parts` replace the display's, with the checks `register_display` makes; one left out stays as it was, and a mistake changes nothing. They go back as they were as soon as the display is turned off, fails or its mod stops. Returns whether it's registered. + set_display_layout: (name: string, placement: {layout: { [HudInstrument]: HudLayout }?, parts: { [HudPart]: HudPartLayout }?}) -> boolean, --- Where the game's instrument `instrument` last drew, in the window's pixels, as the mods' displays place it, and even while one stands in for it; nil before it first draws, or outside a mission. bounds: (instrument: HudInstrument) -> HudBounds?, --- Where the part `part` of the game's instruments last drew, in the window's pixels, as the mods' displays place it, and even while one hides it; nil before it first draws, or outside a mission. @@ -1161,6 +1163,7 @@ export type Interfaces = { height: number, register_display: (name: string, definition: {frame: (seconds: number) -> (), replaces: { HudInstrument }?, layout: { [HudInstrument]: HudLayout }?, parts: { [HudPart]: HudPartLayout }?}) -> string, set_display_enabled: (name: string, enabled: boolean) -> boolean, + set_display_layout: (name: string, placement: {layout: { [HudInstrument]: HudLayout }?, parts: { [HudPart]: HudPartLayout }?}) -> boolean, bounds: (instrument: HudInstrument) -> HudBounds?, part_bounds: (part: HudPart) -> HudBounds?, window_state: (instrument: HudInstrument) -> HudWindowState?, diff --git a/docs/guide/reference.md b/docs/guide/reference.md index 25b50e00..6dc31850 100644 --- a/docs/guide/reference.md +++ b/docs/guide/reference.md @@ -270,6 +270,7 @@ Drawing over the flight display, while it's shown: text, lines and rectangles, i | `height` | number | The window's height, in pixels. | | `register_display(name: string, definition: {frame: (seconds: number) -> (), replaces: { HudInstrument }?, layout: { [HudInstrument]: HudLayout }?, parts: { [HudPart]: HudPartLayout }?})` | string | Registers a display, which `name` qualified with the mod's name names. While the flight display shows, `frame` draws it with this package's functions each frame, until it's turned off with `set_display_enabled`. A failed `frame` turns off that display only. `replaces` lists the game's instruments it stands in for, which aren't drawn while it's on, `layout` moves and scales the instruments it names, and `parts` moves, scales, aligns, rewords or hides the parts of them it names; they keep working, and go back as they were as soon as it's turned off, fails or its mod stops. Returns the qualified name. | | `set_display_enabled(name: string, enabled: boolean)` | boolean | Turns the display `name` on or off: the calling mod's by its own name, or any mod's by the qualified one. Returns whether it's registered. | +| `set_display_layout(name: string, placement: {layout: { [HudInstrument]: HudLayout }?, parts: { [HudPart]: HudPartLayout }?})` | boolean | Changes where the display `name` puts the game's instruments and their parts, after it registers: the calling mod's by its own name, or any mod's by the qualified one. `layout` and `parts` replace the display's, with the checks `register_display` makes; one left out stays as it was, and a mistake changes nothing. They go back as they were as soon as the display is turned off, fails or its mod stops. Returns whether it's registered. | | `bounds(instrument: HudInstrument)` | [HudBounds](#hudbounds), or nil | Where the game's instrument `instrument` last drew, in the window's pixels, as the mods' displays place it, and even while one stands in for it; nil before it first draws, or outside a mission. | | `part_bounds(part: HudPart)` | [HudBounds](#hudbounds), or nil | Where the part `part` of the game's instruments last drew, in the window's pixels, as the mods' displays place it, and even while one hides it; nil before it first draws, or outside a mission. | | `window_state(instrument: HudInstrument)` | [HudWindowState](#hudwindowstate), or nil | How far the game's window that holds `instrument` has opened, and whether it's opening or closing, as the display last moved it on, which it does in every view; for the comms, the further open of its two windows. Nil for an instrument outside the windows, and outside a mission. | @@ -646,6 +647,7 @@ turret, and a turret's own scripts on their turret. | `height` | number | The window's height, in pixels. | | `register_display(name: string, definition: {frame: (seconds: number) -> (), replaces: { HudInstrument }?, layout: { [HudInstrument]: HudLayout }?, parts: { [HudPart]: HudPartLayout }?})` | string | Registers a display, which `name` qualified with the mod's name names. While the flight display shows, `frame` draws it with this package's functions each frame, until it's turned off with `set_display_enabled`. A failed `frame` turns off that display only. `replaces` lists the game's instruments it stands in for, which aren't drawn while it's on, `layout` moves and scales the instruments it names, and `parts` moves, scales, aligns, rewords or hides the parts of them it names; they keep working, and go back as they were as soon as it's turned off, fails or its mod stops. Returns the qualified name. | | `set_display_enabled(name: string, enabled: boolean)` | boolean | Turns the display `name` on or off: the calling mod's by its own name, or any mod's by the qualified one. Returns whether it's registered. | +| `set_display_layout(name: string, placement: {layout: { [HudInstrument]: HudLayout }?, parts: { [HudPart]: HudPartLayout }?})` | boolean | Changes where the display `name` puts the game's instruments and their parts, after it registers: the calling mod's by its own name, or any mod's by the qualified one. `layout` and `parts` replace the display's, with the checks `register_display` makes; one left out stays as it was, and a mistake changes nothing. They go back as they were as soon as the display is turned off, fails or its mod stops. Returns whether it's registered. | | `bounds(instrument: HudInstrument)` | [HudBounds](#hudbounds), or nil | Where the game's instrument `instrument` last drew, in the window's pixels, as the mods' displays place it, and even while one stands in for it; nil before it first draws, or outside a mission. | | `part_bounds(part: HudPart)` | [HudBounds](#hudbounds), or nil | Where the part `part` of the game's instruments last drew, in the window's pixels, as the mods' displays place it, and even while one hides it; nil before it first draws, or outside a mission. | | `window_state(instrument: HudInstrument)` | [HudWindowState](#hudwindowstate), or nil | How far the game's window that holds `instrument` has opened, and whether it's opening or closing, as the display last moved it on, which it does in every view; for the comms, the further open of its two windows. Nil for an instrument outside the windows, and outside a mission. | diff --git a/docs/guide/scripting.md b/docs/guide/scripting.md index bff7ea68..70726423 100644 --- a/docs/guide/scripting.md +++ b/docs/guide/scripting.md @@ -1854,6 +1854,10 @@ hud.register_display("radar", { growing from where it stands on the screen, as the whole display grows with the window. - Everything goes back as it was as soon as the display is turned off, its `frame` fails, or its mod stops. Where two displays place the same instrument, the one registered later does. +- `hud.set_display_layout(name, { layout = ..., parts = ... })` changes a display's `layout` and + `parts` after it registers, such as for a layout that follows the window's size, with the same + checks. Each one the table gives replaces the display's, one it leaves out stays as it was, and a + mistake changes nothing. - `hud.bounds(instrument)` gives where an instrument last drew, in the window's pixels, as the layouts place it, and also while a display stands in for it, so that a display can draw its own in the game's instrument's place. diff --git a/src/scripting/drawing.zig b/src/scripting/drawing.zig index ec9b33e9..ddde7ce6 100644 --- a/src/scripting/drawing.zig +++ b/src/scripting/drawing.zig @@ -535,6 +535,7 @@ pub fn Package(comptime which: Which) type { return @import("registries.zig").show(call, .display, name, enabled); } }.set) else {}; + pub const set_display_layout = if (which == .hud) api.Native("Changes where the display `name` puts the game's instruments and their parts, after it registers: the calling mod's by its own name, or any mod's by the qualified one. `layout` and `parts` replace the display's, with the checks `register_display` makes; one left out stays as it was, and a mistake changes nothing. They go back as they were as soon as the display is turned off, fails or its mod stops. Returns whether it's registered.", "name: string, placement: {layout: { [HudInstrument]: HudLayout }?, parts: { [HudPart]: HudPartLayout }?}", "boolean", @import("registries.zig").setLayout) else {}; pub const replaced = if (which == .hud) instruments.replaced else {}; pub const bounds = if (which == .hud) instruments.bounds else {}; pub const part_bounds = if (which == .hud) instruments.part_bounds else {}; diff --git a/src/scripting/presentation.zig b/src/scripting/presentation.zig index e5205909..a4c24369 100644 --- a/src/scripting/presentation.zig +++ b/src/scripting/presentation.zig @@ -753,6 +753,38 @@ test "a display stands in for the game's instruments and reads what they show" { fixture.shown.endGame(); } +test "a display changes its layout after it registers" { + var fixture: Fixture = undefined; + try fixture.init(&.{.{ + "a", + &.{ + .{ "mod.ini", "[Scripts]\nPlayer=player.luau\n" }, + .{ + "player.luau", + \\local hud = require("openreliant.hud") + \\hud.register_display("radio", {layout = {clock = {scale = 2}}, parts = {radio_face = {scale = 2}}, frame = function() end}) + \\-- The layout follows the window, and the parts stay, as the table leaves them out. + \\assert(hud.set_display_layout("radio", {layout = {clock = {offset = vector.create(5, 0, 0)}}})) + \\-- A mistake changes nothing, and a name not registered isn't one. + \\assert(not pcall(hud.set_display_layout, "radio", {layout = {clock = {scale = 0}}, parts = {}})) + \\assert(not pcall(hud.set_display_layout, "radio", {placement = {}})) + \\assert(not hud.set_display_layout("b:radio", {}) and not hud.set_display_layout("nothing", {})) + }, + }, + }}); + defer fixture.deinit(); + try fixture.shown.startGame(null, fixture.mission.objects, false); + defer fixture.shown.endGame(); + // The displays place the instruments while the flight display's layer shows. + var view: engine.game.camera.Camera = .{}; + var viewing: engine.game.camera.Viewing = .{}; + var host: Host = .{ .seconds = 0.04, .devices = &fixture.devices, .window = .{ 640, 480 }, .camera = .{ .camera = &view, .viewing = &viewing, .now = 1, .player = 0 } }; + host.views.set(.hud, .{ .font = &fixture.font, .gpa = std.testing.allocator, .screen = .{ 640, 480 }, .scale = 1 }); + fixture.shown.frame(host); + try std.testing.expectEqual(hud.Placement{ .offset = .{ 5, 0 } }, fixture.shown.instrumentPlacements().get(.clock)); + try std.testing.expectEqual(hud.Placement{ .scale = 2 }, fixture.shown.partPlacements().get(.radio_face).place); +} + test "a display reads the cockpit's instruments in any view" { const gpa = std.testing.allocator; var fixture: Fixture = undefined; diff --git a/src/scripting/registries.zig b/src/scripting/registries.zig index 8544ef7f..4d5dea96 100644 --- a/src/scripting/registries.zig +++ b/src/scripting/registries.zig @@ -359,6 +359,43 @@ pub fn registration(comptime kind: Kind) fn (api.Call) i32 { }.run; } +/// `hud.set_display_layout(name, placement)`: replaces where the display `name` puts the +/// instruments, its `layout`, and their parts, its `parts`, with the checks `register` makes. A +/// field the table leaves out stays as it was, and a mistake changes nothing +/// ([#1028](https://github.com/OpenReliant/openreliant/issues/1028)). +pub fn setLayout(call: api.Call) i32 { + const state = call.state; + _ = presentation.Presentation.of(call); + const name = values.read(state, []const u8, 1, "name"); + if (state.typeOf(2) != .table) call.raise("expected a table of layout and parts", .{}); + var buffer: [runtime.max_name]u8 = undefined; + const registry = &call.runtime().registries; + const found = if (call.named(name, &buffer)) |qualified| registry.find(.display, qualified) else null; + const index = found orelse { + state.pushBoolean(false); + return 1; + }; + var layout = registry.entries.items[index].layout; + var placed_parts = registry.entries.items[index].parts; + state.pushNil(); + while (state.next(2)) { + const key = (if (state.typeOf(-2) == .string) state.toString(-2) else null) orelse call.raise("placement keys must be names", .{}); + if (std.mem.eql(u8, key, "layout")) { + layout = .initFill(null); + readLayout(call, &layout); + } else if (std.mem.eql(u8, key, "parts")) { + placed_parts = .initFill(null); + readParts(call, &placed_parts); + } else call.raise("unknown placement field '{s}': a display's placement has layout and parts", .{key}); + state.pop(1); + } + const entry = ®istry.entries.items[index]; + entry.layout = layout; + entry.parts = placed_parts; + state.pushBoolean(true); + return 1; +} + pub fn show(call: api.Call, kind: Kind, name: ?[]const u8, enabled: bool) bool { _ = presentation.Presentation.of(call); const registry = &call.runtime().registries;