diff --git a/docs/formats/save.md b/docs/formats/save.md index 1e5a3f00..e60ebbf9 100644 --- a/docs/formats/save.md +++ b/docs/formats/save.md @@ -64,12 +64,11 @@ after the call sign's terminator, which OpenReliant writes as zeros. `Rack1` to `Rack20` are the racks in `saved_racks`' order. Loading the save puts each back over its base where its mod is still on, and keeps the base otherwise. -- **Improvement:** which of the game's variables a saved game keeps is in their records +- **Improvement:** the records of the game's variables say which of them a saved game keeps (`records.game_variables`, [The game's variables](../guide/scripting.md#the-games-variables)). `VARS` always holds the original's 25, so that the original can still read the save, and a load puts back those of them whose records say a saved game keeps them: all 25 with the original's - records. - `VARS` has no room for the others a saved game keeps, such as those mods add + records. `VARS` has no room for the others a saved game keeps, such as those mods add ([Game variables](../guide/modding.md#game-variables)), so the `.mods` file keeps their values under `[Variables]`, each by its name, or by its number where it has none (`save.ModVariables`): diff --git a/docs/guide/modding.md b/docs/guide/modding.md index 64665255..81506efe 100644 --- a/docs/guide/modding.md +++ b/docs/guide/modding.md @@ -924,8 +924,8 @@ A mission can put the player in a ship type based on a ship the player can't fly Basilisk. The player flies it with a cockpit only if it gives `Cockpit`. Its base has no wire frame, wing icon, blind fire or spectral shields, so it has only those it gives, and its `WireFrame` and `WingIcon` pictures are drawn over the Predator's rectangles, as for a type without a base. Without -a `Cockpit`, the player flies it as the game's own such ships, without a cockpit, a wire frame or a -wing icon, and the log names the keys it leaves unused. +a `Cockpit`, the player flies it like the game's own ships of that kind, without a cockpit, a wire +frame or a wing icon, and the log names the keys it leaves unused. A mission can launch ships from a mod's ship type, the player's among them. One that keeps its base's model launches them as its base does, such as from the Reliant's tubes. One with a model of @@ -1099,8 +1099,8 @@ Saved=no - `Saved` says whether a saved game keeps it, `yes` without it. A saved game keeps a mod's variables in its `.mods` file, by their qualified names, such as `pirates:pirates_beaten`. Loading a saved game from before the mod was on gives a variable its `Start`. -- These make the variable's record, beside the game's own in `records.game_variables`, which load - scripts can change, the game's variables too +- These keys make the variable's record in `records.game_variables`, beside the records of the + game's own variables. Load scripts can change any of them ([The game's variables](scripting.md#the-games-variables)). - Scripts read and set the values with `world.variable` and `world.set_variable`, a mod's by its qualified name, or by its own name in the mod's own scripts ([Objects](scripting.md#objects)). The diff --git a/docs/guide/scripting.md b/docs/guide/scripting.md index 70726423..e90d8a1d 100644 --- a/docs/guide/scripting.md +++ b/docs/guide/scripting.md @@ -557,7 +557,6 @@ end read and set the game's variables, as the mission's script does: one of the game's by its name, such as `"krasnaya_alive"`, or its number, and one a mod adds by its qualified name ([Game variables](modding.md#game-variables)). - - `type` is a name such as `"predator"`, or the qualified name of a mod's type, such as `"teapot:teapot"`. - `shields` and `armor` give each quadrant: `left`, `right`, `fore` and `aft`. `hull` is the share @@ -1556,9 +1555,10 @@ records.room_views[2].label = "Report for Duty" ### The game's variables `records.game_variables` says what the campaign does with each of the game's variables, the ones a -mission's script reads and sets by number ([The game's variables](../engine/script-vm.md#the-games-variables)). -It holds one for each number, from 0 to 255, and a script also finds one by its name: the game's, -such as `krasnaya_alive`, or the qualified name of one a mod adds +mission's script reads and sets by number +([The game's variables](../engine/script-vm.md#the-games-variables)). It holds one for each +number, from 0 to 255, and a script also finds one by its name: the game's, such as +`krasnaya_alive`, or the qualified name of one a mod adds ([Game variables](modding.md#game-variables)). Each has: - `name`, its name, or nil for a number without one. It can't be changed. @@ -1580,8 +1580,8 @@ records.game_variables.countdown = { attempt = 60 } - The table always has its 256 variables: a script changes them but can't add or remove them. A mod adds a variable of its own, with a name, in its manifest. -- A saved game keeps the 25 variables the original keeps where the original keeps them, so that the - original can still read the save, and the others it keeps in its `.mods` file +- A saved game keeps the original's 25 variables where the original keeps them, so that the + original can still read the save, and any others in its `.mods` file ([Saved games](../formats/save.md#in-openreliant)). - Scripts read and set the values in a mission that runs with `world.variable` and `world.set_variable` ([Objects](#objects)). diff --git a/src/engine/game/additions.zig b/src/engine/game/additions.zig index 89a237e6..73201969 100644 --- a/src/engine/game/additions.zig +++ b/src/engine/game/additions.zig @@ -77,10 +77,10 @@ pub fn Spec(comptime Base: type, comptime Extra: type) type { end: u32, /// The least number the mod's own files can give one, where that is below `first`. own_first: ?u32 = null, - /// The game's record the manifest's text names, and the number of each. Null for a family - /// whose records have no base, such as the game variables, which take `template`. - baseOf: ?fn ([]const u8) ?Base = null, - baseNumber: ?fn (Base) u32 = null, + /// How the manifest names the game's records that the family's records take as their base; + /// null for a family whose records have no base, such as the game variables, which take + /// `template`. + bases: ?Bases(Base) = null, /// What the mod's own files that use the number after a record's name are, for the log. numbered_in: []const u8, /// The game's record one without a `Base` starts from, where the family takes one without: @@ -92,6 +92,17 @@ pub fn Spec(comptime Base: type, comptime Extra: type) type { }; } +/// How a family's manifest names the game's records that its records take as their base +/// (`Spec.bases`). +pub fn Bases(comptime Base: type) type { + return struct { + /// The game's record that the manifest's text names. + of: fn ([]const u8) ?Base, + /// The number of one of the game's records. + number: fn (Base) u32, + }; +} + /// What a family's `Spec.readExtra` reads with: the arena, the mod and its manifest, and the /// record's own name, for the log. pub const Context = struct { @@ -236,10 +247,10 @@ pub fn Family(comptime Base_: type, comptime Extra: type, comptime spec: Spec(Ba /// The number of what `text` names, for a record of the mod whose qualified names start /// with `mod` (`Mod.qualifier`): one of the game's by its name or number - /// (`Spec.baseOf`), one the mods add by its qualified name, or one `mod` adds by its own + /// (`Spec.bases`), one the mods add by its qualified name, or one `mod` adds by its own /// name. pub fn named(text: []const u8, mod: []const u8) ?u32 { - if (spec.baseOf) |of| if (of(text)) |base| return spec.baseNumber.?(base); + if (spec.bases) |bases| if (bases.of(text)) |base| return bases.number(base); if (find(text)) |number| return number; var buffer: [256]u8 = undefined; const qualified = std.mem.print(&buffer, "{s}:{s}", .{ mod, text }) catch return null; @@ -348,8 +359,8 @@ pub fn Family(comptime Base_: type, comptime Extra: type, comptime spec: Spec(Ba return null; }; const section = try context.arena.print("{s}{s}", .{ spec.item_section, own }); - const base_text = if (spec.baseOf == null) null else manifest.value(section, "Base"); - const base = if (base_text) |text| spec.baseOf.?(text) orelse { + const base_text = if (spec.bases == null) null else manifest.value(section, "Base"); + const base = if (base_text) |text| spec.bases.?.of(text) orelse { context.warn(spec.noun, "has the base '{s}', which isn't one of the game's", .{text}); return null; } else spec.template orelse { @@ -380,7 +391,7 @@ pub fn Family(comptime Base_: type, comptime Extra: type, comptime spec: Spec(Ba const kept = @min(game.len, own_count); for (made[0..kept], game[0..kept]) |*record, from| record.* = from; @memset(made[kept..own_count], std.mem.zeroes(Record)); - for (made[own_count..], registered) |*record, each| record.* = made[spec.baseNumber.?(each.base) - numbered_from]; + for (made[own_count..], registered) |*record, each| record.* = made[spec.bases.?.number(each.base) - numbered_from]; return made; } }; @@ -451,17 +462,12 @@ fn readShip(context: Context, section: []const u8, base: gameobj.GameType) Alloc if (manifest.value(section, "WireFrame")) |text| made.wire_frame = try context.arena.dupe(u8, files.stem(text)); if (manifest.value(section, "WingIcon")) |text| made.wing_icon = try context.arena.dupe(u8, files.stem(text)); if (manifest.value(section, "EngineSound")) |name| made.engine_sound = try readSound(context, "ship type", name) orelse return null; - if (manifest.value(section, "Tier")) |text| made.tier = try readTier(context, "ship type", text) orelse return null; + if (manifest.value(section, "Tier")) |text| made.tier = readNumber(u2, context, "ship type", "Tier", text) orelse return null; if (manifest.value(section, "BlindFire")) |text| made.blind_fire = readSwitch(context, "ship type", "BlindFire", text) orelse return null; if (manifest.value(section, "SpectralShields")) |text| made.spectral_shields = readSwitch(context, "ship type", "SpectralShields", text) orelse return null; if (manifest.value(section, "Class")) |text| made.class = readNamed(loadout.Class, context, "Class", text) orelse return null; if (manifest.value(section, "Access")) |text| made.access = readNamed(loadout.Access, context, "Access", text) orelse return null; - if (manifest.value(section, "Crew")) |text| { - made.crew = std.fmt.parseInt(u8, std.mem.trim(u8, text, " \t"), 10) catch { - context.warn("ship type", "gives the Crew '{s}', which isn't a number from 0 to 255", .{text}); - return null; - }; - } + if (manifest.value(section, "Crew")) |text| made.crew = readNumber(u8, context, "ship type", "Crew", text) orelse return null; if (manifest.value(section, "Schematic")) |text| made.schematic = try fileName(context, text); if (manifest.value(section, "GunsModel")) |text| made.guns_model = try fileName(context, text); if (manifest.value(section, "Guns")) |text| { @@ -480,7 +486,7 @@ fn readShip(context: Context, section: []const u8, base: gameobj.GameType) Alloc } // A type based on a ship the player can't fly is flown only with a cockpit of its own // (`main.playerShip`), so without one, what it gives for the player's ship goes unused. - if (made.cockpit == null and @backingInt(base.untwinned()) >= loadout.ship_count) { + if (made.cockpit == null and !base.playerFlies()) { for ([_][]const u8{ "WireFrame", "WingIcon", "BlindFire", "SpectralShields" }) |key| { if (manifest.value(section, key) != null) context.warn("ship type", "gives {s}, which a ship the player can't fly uses only with a Cockpit", .{key}); } @@ -506,7 +512,7 @@ fn readMissile(context: Context, section: []const u8, _: missiles_module.GameMis if (manifest.value(section, "Model")) |model| made.model = try fileName(context, model); if (manifest.value(section, "Pod")) |pod| made.pod = try fileName(context, pod); if (manifest.value(section, "Description")) |text| made.description = try context.arena.dupe(u8, text); - if (manifest.value(section, "Tier")) |text| made.tier = try readTier(context, "missile", text) orelse return null; + if (manifest.value(section, "Tier")) |text| made.tier = readNumber(u2, context, "missile", "Tier", text) orelse return null; return made; } @@ -541,11 +547,11 @@ fn readNamed(comptime Named: type, context: Context, comptime key: []const u8, t return null; } -/// The campaign tier `text` names, for a record of `noun`; null where it isn't one, which the log -/// says. -fn readTier(context: Context, comptime noun: []const u8, text: []const u8) Allocator.Error!?u2 { - return std.fmt.parseInt(u2, std.mem.trim(u8, text, " \t"), 10) catch { - context.warn(noun, "gives the tier '{s}', which isn't from 0 to 3", .{text}); +/// `text` as the whole number of `T` that a record of `noun` gives its `key`, such as a campaign +/// tier, a `u2`; null where it isn't one, which the log says with the numbers `T` holds. +fn readNumber(comptime T: type, context: Context, comptime noun: []const u8, comptime key: []const u8, text: []const u8) ?T { + return std.fmt.parseInt(T, std.mem.trim(u8, text, " \t"), 0) catch { + context.warn(noun, "gives " ++ key ++ " the value '{s}', which isn't a whole number from {d} to {d}", .{ text, std.math.minInt(T), std.math.maxInt(T) }); return null; }; } @@ -557,16 +563,18 @@ pub const ships = Family(gameobj.GameType, ShipExtra, .{ .item_section = "ShipType ", .first = 0x100, .end = @backingInt(gameobj.GameType.sun_marker), - .baseOf = struct { - fn of(text: []const u8) ?gameobj.GameType { - return gameNamed(gameobj.GameType, text, 0x100); - } - }.of, - .baseNumber = struct { - fn number(base: gameobj.GameType) u32 { - return @backingInt(base); - } - }.number, + .bases = .{ + .of = struct { + fn of(text: []const u8) ?gameobj.GameType { + return gameNamed(gameobj.GameType, text, 0x100); + } + }.of, + .number = struct { + fn number(base: gameobj.GameType) u32 { + return @backingInt(base); + } + }.number, + }, .numbered_in = "its missions", // A fighter the game singles out nowhere. .template = .predator, @@ -636,21 +644,23 @@ pub const guns = Family(guns_module.GameGun, GunExtra, .{ .item_section = "Gun ", .first = guns_module.max_types, .end = std.math.maxInt(u8) + 1, - .baseOf = struct { - fn of(text: []const u8) ?guns_module.GameGun { - const trimmed = std.mem.trim(u8, text, " \t"); - if (std.fmt.parseInt(u32, trimmed, 0)) |number| { - if (number == 0 or number >= guns_module.max_types) return null; - return @fromBackingInt(@intCast(number - 1)); - } else |_| {} - return gameNamed(guns_module.GameGun, trimmed, guns_module.max_types); - } - }.of, - .baseNumber = struct { - fn number(base: guns_module.GameGun) u32 { - return base.number(); - } - }.number, + .bases = .{ + .of = struct { + fn of(text: []const u8) ?guns_module.GameGun { + const trimmed = std.mem.trim(u8, text, " \t"); + if (std.fmt.parseInt(u32, trimmed, 0)) |number| { + if (number == 0 or number >= guns_module.max_types) return null; + return @fromBackingInt(@intCast(number - 1)); + } else |_| {} + return gameNamed(guns_module.GameGun, trimmed, guns_module.max_types); + } + }.of, + .number = struct { + fn number(base: guns_module.GameGun) u32 { + return base.number(); + } + }.number, + }, .readExtra = readGun, .numbered_in = "its models' muzzles", }); @@ -662,17 +672,19 @@ pub const missiles = Family(missiles_module.GameMissile, MissileExtra, .{ .item_section = "Missile ", .first = missiles_module.type_count, .end = std.math.maxInt(u8) + 1, - .baseOf = struct { - fn of(text: []const u8) ?missiles_module.GameMissile { - const base = gameNamed(missiles_module.GameMissile, text, missiles_module.type_count) orelse return null; - return if (base == .none) null else base; - } - }.of, - .baseNumber = struct { - fn number(base: missiles_module.GameMissile) u32 { - return @intCast(@backingInt(base)); - } - }.number, + .bases = .{ + .of = struct { + fn of(text: []const u8) ?missiles_module.GameMissile { + const base = gameNamed(missiles_module.GameMissile, text, missiles_module.type_count) orelse return null; + return if (base == .none) null else base; + } + }.of, + .number = struct { + fn number(base: missiles_module.GameMissile) u32 { + return @intCast(@backingInt(base)); + } + }.number, + }, .numbered_in = "its models' hardpoints", .readExtra = readMissile, }); @@ -707,17 +719,19 @@ pub const pilots = Family(u8, PilotExtra, .{ .item_section = "Pilot ", .first = pilots_module.Table.count, .end = dte.Ship.no_pilot, - .baseOf = struct { - fn of(text: []const u8) ?u8 { - const number = std.fmt.parseInt(u8, std.mem.trim(u8, text, " \t"), 0) catch return null; - return if (number < pilots_module.Table.count) number else null; - } - }.of, - .baseNumber = struct { - fn number(base: u8) u32 { - return base; - } - }.number, + .bases = .{ + .of = struct { + fn of(text: []const u8) ?u8 { + const number = std.fmt.parseInt(u8, std.mem.trim(u8, text, " \t"), 0) catch return null; + return if (number < pilots_module.Table.count) number else null; + } + }.of, + .number = struct { + fn number(base: u8) u32 { + return base; + } + }.number, + }, .numbered_in = "its missions", .readExtra = readPilot, }); @@ -729,21 +743,12 @@ pub const pilots = Family(u8, PilotExtra, .{ fn readVariable(context: Context, section: []const u8, _: void) Allocator.Error!?gameflow.VariableRecord { const manifest = context.mod.manifest; var made: gameflow.VariableRecord = .{ .start = 0, .saved = true }; - if (manifest.value(section, "Start")) |text| made.start = readValue(context, "Start", text) orelse return null; - if (manifest.value(section, "Attempt")) |text| made.attempt = readValue(context, "Attempt", text) orelse return null; + if (manifest.value(section, "Start")) |text| made.start = readNumber(i32, context, "game variable", "Start", text) orelse return null; + if (manifest.value(section, "Attempt")) |text| made.attempt = readNumber(i32, context, "game variable", "Attempt", text) orelse return null; if (manifest.value(section, "Saved")) |text| made.saved = readSwitch(context, "game variable", "Saved", text) orelse return null; return made; } -/// The value `text` gives a game variable's `key`, a whole number; null where it isn't one, which -/// the log says. -fn readValue(context: Context, comptime key: []const u8, text: []const u8) ?i32 { - return std.fmt.parseInt(i32, std.mem.trim(u8, text, " \t"), 0) catch { - context.warn("game variable", "gives " ++ key ++ " the value '{s}', which isn't a whole number", .{text}); - return null; - }; -} - /// The game variables mods add, past the original's block of 64 up to the last number a script's /// byte names (`vm.Variables`). A mod's missions can also number them from the block's spare /// variables on, which neither the engine nor the shipped missions use, so that the missions run in @@ -1020,8 +1025,8 @@ test "a type based on a ship the player can't fly, with and without a cockpit" { } }; try read(arena.allocator(), &.{mod}); defer reset(); - // Both are read. The second, without a cockpit, leaves its wire frame and blind fire unused, which - // the log says. + // Both are read. The second, without a cockpit, leaves its wire frame and blind fire unused, + // which the log says. const list = ships.all(); try std.testing.expectEqual(2, list.len); try std.testing.expectEqualStrings("kamg_frm.shp", list[0].extra.cockpit.?); diff --git a/src/engine/game/gameflow.zig b/src/engine/game/gameflow.zig index 8b2b6fa7..e6d902fd 100644 --- a/src/engine/game/gameflow.zig +++ b/src/engine/game/gameflow.zig @@ -116,13 +116,19 @@ pub fn addModVariables(records: *[vm.Variables.count]VariableRecord) void { /// `campaign_new` (`0x004751B0`) as a new campaign begins, which `WinMain` also runs as the game /// starts: gives each of the game's variables the value its record gives a new campaign /// (`VariableRecord.start`). The original's records clear the first 32 and set the campaign's -/// flags (`campaign_flags`). `Campaign.begin` sets up the rest of the campaign: mission 1 as the next, -/// and empty records for each mission. `save.Game.clearPilot` clears the pilot's tallies, and -/// `ProfileFile.open` reads the pilot's profile. +/// flags (`campaign_flags`). `Campaign.begin` sets up the rest of the campaign: mission 1 as the +/// next, and empty records for each mission. `save.Game.clearPilot` clears the pilot's tallies, +/// and `ProfileFile.open` reads the pilot's profile. pub fn newCampaign(variables: *vm.Variables) void { + setFromRecords(variables, .start); +} + +/// Sets each of the game's variables in `variables` to the value its record gives in `given`, +/// where it gives one (`VariableRecord`). +fn setFromRecords(variables: *vm.Variables, comptime given: enum { start, attempt }) void { for (0..vm.Variables.count) |at| { const number: u8 = @intCast(at); - if (variableRecord(number).start) |value| variables.slot(number).* = @bitCast(value); + if (@field(variableRecord(number), @tagName(given))) |value| variables.slot(number).* = @bitCast(value); } } @@ -278,10 +284,7 @@ const mission_tiers = [last_mission]u2{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 /// (`VariableRecord.attempt`). The original's records clear the attempt's own /// (`attempt_variables`). pub fn resetVariables(variables: *vm.Variables) void { - for (0..vm.Variables.count) |at| { - const number: u8 = @intCast(at); - if (variableRecord(number).attempt) |value| variables.slot(number).* = @bitCast(value); - } + setFromRecords(variables, .attempt); } /// The campaign as it goes from one mission to the next. diff --git a/src/engine/game/gameflow/save.zig b/src/engine/game/gameflow/save.zig index 5f4d9137..0e169283 100644 --- a/src/engine/game/gameflow/save.zig +++ b/src/engine/game/gameflow/save.zig @@ -342,8 +342,8 @@ pub const ModChoice = struct { /// The values of the game's variables that a saved game keeps (`gameflow.VariableRecord.saved`) /// but `VARS` has no room for: those mods add /// ([#1322](https://github.com/OpenReliant/openreliant/issues/1322)), and any other that a mod's -/// load script has a saved game keep. The `.mods` file (`Mods`) keeps them under `[Variables]`, each -/// by its name (`vm.Variable.scriptName`), or by its number where it has none. +/// load script marks as saved. The `.mods` file (`Mods`) keeps them under `[Variables]`, each by +/// its name (`vm.Variable.scriptName`), or by its number where it has none. pub const ModVariables = struct { /// Each variable's value, by its number; null where the save has none. values: [vm.Variables.count]?i32 = @splat(null), @@ -351,8 +351,8 @@ pub const ModVariables = struct { /// The `.mods` file's section. const section = "Variables"; - /// Whether the save keeps the variable `number` here: one the records have a saved game keep - /// that `VARS` has no room for. + /// Whether the save keeps the variable `number` here: one whose record says a saved game keeps + /// it, and that `VARS` has no room for. fn keeps(number: u8) bool { return gameflow.variableRecord(number).saved and !in_vars.isSet(number); } @@ -367,8 +367,8 @@ pub const ModVariables = struct { return kept; } - /// Puts the values back into `variables`. A variable the save keeps here that it has no value - /// for, as in a save from before its mod was on, takes the value a new campaign gives it, if any. + /// Puts the values back into `variables`. A variable the save keeps here but has no value for, + /// as in a save from before its mod was on, takes the value a new campaign gives it, if any. fn apply(kept: ModVariables, variables: *vm.Variables) void { for (kept.values, 0..) |value, at| { const number: u8 = @intCast(at); @@ -571,9 +571,9 @@ pub const Game = struct { /// What `game_load` puts back of `save` (`0x00475488` to `0x004755F0`): each record, then the /// game's variables 0 to 31 cleared and those the save keeps put back. The rest keep what they - /// held. Which of them the save keeps is their records' (`gameflow.VariableRecord.saved`): the - /// original's keep the 25 of `VARS`, and OpenReliant puts back the others that a saved game - /// keeps from the `.mods` file (`ModVariables`). + /// held. Their records say which the save keeps (`gameflow.VariableRecord.saved`): with the + /// original's records, the 25 of `VARS`. OpenReliant puts back the others a saved game keeps + /// from the `.mods` file (`ModVariables`). /// /// **Fix:** a ship or a missile past those the game has is taken as the Predator, or none, /// where the game reads past its tables with it; so are a rank and a tier (`Miss.pilotRank`). @@ -760,11 +760,11 @@ pub const Folder = struct { } /// Writes `save` as saved game `slot` of `call_sign` (`game_save`), with its `.mods` file - /// where it holds anything of the mods' records (`Mods`), and what - /// `extra` keeps beside it. The files that go with the save are written first and the save - /// next, so a new save is only listed once they're all there, and a failure of any of them - /// fails the save and leaves the slot as it was. The files an earlier save of the slot had, - /// and this one doesn't, are removed last. + /// where it holds anything of the mods' records (`Mods`), and what `extra` keeps beside it. + /// The files that go with the save are written first and the save next, so a new save is only + /// listed once they're all there, and a failure of any of them fails the save and leaves the + /// slot as it was. The files an earlier save of the slot had, and this one doesn't, are removed + /// last. pub fn store(folder: Folder, gpa: std.mem.Allocator, call_sign: []const u8, slot: u8, save: *const Save) StoreError!void { const bytes = try write(gpa, save); defer gpa.free(bytes); @@ -1109,8 +1109,8 @@ test ModVariables { defer gpa.free(text); try std.testing.expectEqualStrings("[Variables]\ncountdown=30\na:beaten=-7\n", text); - // Loaded with a variable the mod added since, that one starts as in a new campaign. Ghost's flag - // isn't put back, so the load clears it, as it clears the first 32. + // Loaded with a variable the mod added since, that one starts as in a new campaign. Ghost's + // flag isn't put back, so the load clears it, as it clears the first 32. additions.variables.install(&list); gameflow.addModVariables(&records); var back = empty(); diff --git a/src/engine/game/gameobj.zig b/src/engine/game/gameobj.zig index 9d8c02dc..495c3e3a 100644 --- a/src/engine/game/gameobj.zig +++ b/src/engine/game/gameobj.zig @@ -745,6 +745,13 @@ pub const GameType = enum(u32) { /// How many of the player's ship types have twins: the first twelve, from 0. pub const player_twins = 0x100 - player_twins_first; + /// Whether the player can fly a ship of this type, a twin as the ship it twins: one of the + /// first twelve, the Predator to the Phoenix, which the start fits a cockpit to + /// (`main.player_ships`). + pub fn playerFlies(object_type: GameType) bool { + return object_type.untwinned().number() < player_twins; + } + /// Its twin among the second set of the player's ship types, for one of the first set's; null /// for any other. pub fn twin(object_type: GameType) ?GameType { @@ -869,8 +876,7 @@ pub const Type = enum(u32) { /// The name scripts know it by, if it has one: its qualified name for a type a mod adds. pub fn scriptName(object_type: Type) ?[]const u8 { if (object_type.added()) |from_mod| return from_mod.name; - const tag_name = std.enums.tagName(GameType, object_type.base()) orelse return null; - return if (tag_name[0] == '_') null else tag_name; + return layout.tagName(GameType, object_type.base()); } /// The type scripts name `text`, if there is one. @@ -1663,6 +1669,13 @@ test "Type.twin" { try std.testing.expectEqual(null, Type.of(.t_phoenix).twin()); } +test "GameType.playerFlies" { + // The Predator to the Phoenix and their twins, and nothing past them. + try std.testing.expect(GameType.predator.playerFlies() and GameType.phoenix.playerFlies()); + try std.testing.expect(GameType.t_phoenix.playerFlies()); + try std.testing.expect(!GameType.reliant.playerFlies() and !GameType.basilisk.playerFlies()); +} + test "GameType.rock" { try std.testing.expectEqual(.asteroid, GameType.asteroid_7.rock()); try std.testing.expectEqual(.turret, GameType.turret_asteroid_1.rock()); diff --git a/src/engine/game/hudoptions.zig b/src/engine/game/hudoptions.zig index 200dbdb2..79fc0dc9 100644 --- a/src/engine/game/hudoptions.zig +++ b/src/engine/game/hudoptions.zig @@ -201,8 +201,8 @@ pub const PauseMenu = struct { /// `pause_menu_draw` (`0x004906F0`), the overlay while paused, with the pointer brought up to /// date first as `mission_paused_frame` does (`menu_mouse_update`). A screen shown for the /// first time runs its enter routine; if its choice goes elsewhere, it runs its leave routine. - /// The game draws the pointer last, which OpenReliant draws after what scripts draw over the - /// menu (`drawPointer`). + /// The game draws the pointer last; OpenReliant draws it after what scripts draw over the menu + /// (`drawPointer`). pub fn draw(pause_menu: *PauseMenu, frame: Frame) menu.Error!void { pause_menu.pointer_shown = null; const fonts = if (pause_menu.fonts) |*open_fonts| open_fonts else return; @@ -213,7 +213,7 @@ pub const PauseMenu = struct { .outcome => return, }; const tab = SettingsScreen.tabOf(screen); - // The settings screen the frame before left goes, now that its pointer has been drawn. + // The settings screen that the frame before left closes, now that its pointer is drawn. if (tab == null) if (pause_menu.settings) |*shown| { shown.close(fonts.gpa); pause_menu.settings = null; diff --git a/src/engine/game/interface/game_modes.zig b/src/engine/game/interface/game_modes.zig index c6b82702..690c83b8 100644 --- a/src/engine/game/interface/game_modes.zig +++ b/src/engine/game/interface/game_modes.zig @@ -181,7 +181,8 @@ pub const GameModes = struct { } /// The screen's drawing, over the background: the title, the list and the panel, the buttons, - /// the one under the pointer lit, and OpenReliant's version. The pointer goes over it all, after what scripts draw (`canvas.Canvas.drawPointer`). + /// the one under the pointer lit, and OpenReliant's version. The pointer goes over it all, + /// after what scripts draw (`canvas.Canvas.drawPointer`). pub fn draw(screen: GameModes, canvas: Canvas, art: *hud.Art) canvas_module.Error!void { const modes = screen.modes; try title.write(canvas, canvas.fonts.large, canvas_module.white); diff --git a/src/engine/game/interface/game_options.zig b/src/engine/game/interface/game_options.zig index c21d2713..847d7698 100644 --- a/src/engine/game/interface/game_options.zig +++ b/src/engine/game/interface/game_options.zig @@ -131,8 +131,9 @@ pub const GameOptions = struct { /// `game_options_draw` (`0x0042AFB0`), over the menu's background: the buttons, the item under /// the pointer lit, the title and the labels, ABOUT OPENRELIANT's box or QUIT's question where - /// either is up, and OpenReliant's version. The pointer goes over it all, after what scripts draw (`canvas.Canvas.drawPointer`). The game writes the label under the - /// pointer white before the labels, which write over it in blue, so it stays blue. + /// either is up, and OpenReliant's version. The pointer goes over it all, after what scripts + /// draw (`canvas.Canvas.drawPointer`). The game writes the label under the pointer white + /// before the labels, which write over it in blue, so it stays blue. pub fn draw(menu: GameOptions, canvas: Canvas, art: *hud.Art, dialog_art: *hud.Art, about_art: *hud.Art) canvas_module.Error!void { for ([_]Item{ .about, .mods, .main_menu, .quit }) |item| { const rect = rects.get(item); diff --git a/src/engine/game/interface/in_game_options.zig b/src/engine/game/interface/in_game_options.zig index 6278a85d..45b7cdf5 100644 --- a/src/engine/game/interface/in_game_options.zig +++ b/src/engine/game/interface/in_game_options.zig @@ -243,7 +243,8 @@ pub const InGameOptions = struct { /// `in_game_options_draw` (`0x004398B0`), over the menu's background: the buttons and their /// labels, the item under the pointer lit, the panels' labels, ABOUT OPENRELIANT's box or - /// QUIT's question where either is up, and OpenReliant's version. The pointer goes over it all, after what scripts draw (`canvas.Canvas.drawPointer`). + /// QUIT's question where either is up, and OpenReliant's version. The pointer goes over it + /// all, after what scripts draw (`canvas.Canvas.drawPointer`). pub fn draw(menu: InGameOptions, canvas: Canvas, art: *hud.Art, dialog_art: *hud.Art, about_art: ?*hud.Art) canvas_module.Error!void { for (std.enums.values(Item)) |item| { const shown = button(item) orelse continue; diff --git a/src/engine/game/interface/mod_catalogue.zig b/src/engine/game/interface/mod_catalogue.zig index 88b98572..fe5667f9 100644 --- a/src/engine/game/interface/mod_catalogue.zig +++ b/src/engine/game/interface/mod_catalogue.zig @@ -788,9 +788,10 @@ pub const ModCatalogue = struct { }; } - /// Draws the screen over the background: the title, the list, the panel, the buttons with the one - /// under the pointer highlighted and the ones that do nothing dimmed, and OpenReliant's version. - /// The pointer goes over it all, after what scripts draw (`canvas.Canvas.drawPointer`). + /// Draws the screen over the background: the title, the list, the panel, the buttons with the + /// one under the pointer highlighted and the ones that do nothing dimmed, and OpenReliant's + /// version. The pointer goes over it all, after what scripts draw + /// (`canvas.Canvas.drawPointer`). pub fn draw(screen: ModCatalogue, canvas: Canvas, art: *hud.Art) canvas_module.Error!void { try title.write(canvas, canvas.fonts.large, canvas_module.white); list_frame.draw(canvas); diff --git a/src/engine/game/interface/mod_manager.zig b/src/engine/game/interface/mod_manager.zig index 77c63082..2f64f4d7 100644 --- a/src/engine/game/interface/mod_manager.zig +++ b/src/engine/game/interface/mod_manager.zig @@ -631,7 +631,8 @@ pub const ModManager = struct { /// The screen's drawing, over the background: the title, the note while the mods wait for the /// next start, the list and the panel, the buttons, the one under the pointer lit, and - /// OpenReliant's version. The pointer goes over it all, after what scripts draw (`canvas.Canvas.drawPointer`). + /// OpenReliant's version. The pointer goes over it all, after what scripts draw + /// (`canvas.Canvas.drawPointer`). pub fn draw(screen: ModManager, canvas: Canvas, art: *hud.Art) canvas_module.Error!void { try title.write(canvas, canvas.fonts.large, canvas_module.white); if (screen.waits()) try restart_note.write(canvas, canvas.fonts.small, canvas_module.gold); diff --git a/src/engine/game/interface/mod_options.zig b/src/engine/game/interface/mod_options.zig index 2368dfdf..12902117 100644 --- a/src/engine/game/interface/mod_options.zig +++ b/src/engine/game/interface/mod_options.zig @@ -485,8 +485,9 @@ pub const ModOptions = struct { return buffer[0..count]; } - /// The screen's drawing, over the background: the title, the list, the buttons, the one under the - /// pointer lit, the text of the option under it, and OpenReliant's version. The pointer goes over it all, after what scripts draw (`canvas.Canvas.drawPointer`). + /// The screen's drawing, over the background: the title, the list, the buttons, the one under + /// the pointer lit, the text of the option under it, and OpenReliant's version. The pointer + /// goes over it all, after what scripts draw (`canvas.Canvas.drawPointer`). pub fn draw(screen: *const ModOptions, canvas: Canvas, art: *hud.Art) canvas_module.Error!void { try (Label{ .text = .{ .words = screen.page.title }, .at = title_at, .alignment = .centre }).write(canvas, canvas.fonts.large, canvas_module.white); var buffer: [max_options]Line.Shown = undefined; diff --git a/src/engine/game/interface/restart.zig b/src/engine/game/interface/restart.zig index 755f0662..a62e7557 100644 --- a/src/engine/game/interface/restart.zig +++ b/src/engine/game/interface/restart.zig @@ -122,9 +122,10 @@ pub const Restart = struct { /// picture, the button under the pointer lit, and the three labels in the small font. The /// pointer goes over it, after what scripts draw (`drawPointer`). pub fn draw(screen: Restart, canvas: Canvas, art: *hud.Art) canvas_module.Error!void { - try screen.faded(canvas).shape(art, picture_shape, picture_at); - if (screen.under) |under| try screen.faded(canvas).shape(art, lit_shape, lit_at.get(under)); - for (labels.values) |label| try label.write(screen.faded(canvas), canvas.fonts.small, label_colour); + const dimmed = screen.faded(canvas); + try dimmed.shape(art, picture_shape, picture_at); + if (screen.under) |under| try dimmed.shape(art, lit_shape, lit_at.get(under)); + for (labels.values) |label| try label.write(dimmed, canvas.fonts.small, label_colour); } /// The end of `restart_screen_draw`: the pointer, at the screen's brightness, until a choice is diff --git a/src/engine/game/interface/saved_games.zig b/src/engine/game/interface/saved_games.zig index 272d22de..8faa0009 100644 --- a/src/engine/game/interface/saved_games.zig +++ b/src/engine/game/interface/saved_games.zig @@ -560,7 +560,8 @@ pub const SavedGames = struct { /// title; saving, the name's frame and, while it is typed, the name with its cursor and OK; /// the labels and the date; the buttons, the one under the pointer lit with its label in white; /// the rows shown, the selected one white over a bar; the selected save's details; the arrows; - /// QUIT's question or the box saying a save failed; and OpenReliant's version. The pointer goes over it all, after what scripts draw (`canvas.Canvas.drawPointer`). + /// QUIT's question or the box saying a save failed; and OpenReliant's version. The pointer + /// goes over it all, after what scripts draw (`canvas.Canvas.drawPointer`). pub fn draw(screen: SavedGames, canvas: Canvas, art: *hud.Art, dialog_art: *hud.Art, call_sign: []const u8) canvas_module.Error!void { const large = canvas.fonts.large; const small = canvas.fonts.small; diff --git a/src/engine/game/interface/settings.zig b/src/engine/game/interface/settings.zig index 80e45219..9f7bc6f5 100644 --- a/src/engine/game/interface/settings.zig +++ b/src/engine/game/interface/settings.zig @@ -566,7 +566,8 @@ pub const Settings = struct { /// The screen's drawing (`controls_screen_draw`, `0x0042CD30`, `audio_screen_draw`, /// `0x0042E2E0`, `video_screen_draw`, `0x0042F440`): the tabs' labels, the shown one white and /// one under the pointer gold, the tab, the buttons, the one under the pointer lit, Escape's - /// question where it is up, and OpenReliant's version. The pointer goes over it all, after what scripts draw (`canvas.Canvas.drawPointer`). + /// question where it is up, and OpenReliant's version. The pointer goes over it all, after + /// what scripts draw (`canvas.Canvas.drawPointer`). pub fn draw(screen: Settings, canvas: Canvas, art: *hud.Art, dialog_art: *hud.Art, shown: Shown) canvas_module.Error!void { for (std.enums.values(Tab)) |tab| { const colour = if (tab == screen.tab) canvas_module.white else if (screen.lit_tab == tab) canvas_module.gold else canvas_module.blue; diff --git a/src/engine/game/main.zig b/src/engine/game/main.zig index 4180d7e2..337d102e 100644 --- a/src/engine/game/main.zig +++ b/src/engine/game/main.zig @@ -34,7 +34,6 @@ const create = @import("create.zig"); const pilots = @import("pilots.zig"); const gameobj = @import("gameobj.zig"); const additions = @import("additions.zig"); -const loadout_tables = @import("../interface/loadout/tables.zig"); const guns = @import("guns.zig"); const launch = @import("launch.zig"); const cloak = @import("cloak.zig"); @@ -2220,8 +2219,8 @@ pub const player_ships = [_]PlayerShip{ }; comptime { - // They are the loadout's twelve, which the mods' manifests count on (`additions.readShip`). - std.debug.assert(player_ships.len == loadout_tables.ship_count); + // One for each ship the player can fly (`gameobj.GameType.playerFlies`). + std.debug.assert(player_ships.len == gameobj.GameType.player_twins); } /// What the start fits in mission 25's first part, where the player's wing flies Kamovs @@ -2247,10 +2246,10 @@ pub fn cockpitShip(all: *const create.Objects, ship_type: gameobj.Type) ?PlayerS /// (`additions.ShipExtra`); without a base it carries no device its manifest doesn't give. A type /// based on a ship the player can't fly is flown with a cockpit of its own (`ownShip`). pub fn playerShip(ship_type: gameobj.Type) ?PlayerShip { - const index = @backingInt(ship_type.base().untwinned()); + const base = ship_type.base(); const mod = ship_type.added(); - if (index >= player_ships.len) return ownShip(mod orelse return null); - var ship = player_ships[index]; + if (!base.playerFlies()) return ownShip(mod orelse return null); + var ship = player_ships[base.untwinned().number()]; const added = mod orelse return ship; // A type without a base carries none of its template's devices. if (!added.based) { @@ -2261,8 +2260,8 @@ pub fn playerShip(ship_type: gameobj.Type) ?PlayerShip { return ship; } -/// The ship whose display shapes the pictures of a mod's type based on a ship the player can't fly -/// stand in for (`ownShip`): the Predator, as for a type without a base. +/// The ship whose wire frame and wing icon the pictures of a mod's type replace, where the type is +/// based on a ship the player can't fly (`ownShip`): the Predator, as for a type without a base. const stand_in_ship = player_ships[@backingInt(gameobj.GameType.predator)]; /// The player's ship of `added`, a mod's type based on a ship the player can't fly, such as the diff --git a/src/engine/vm.zig b/src/engine/vm.zig index 7d9625d8..43a275e7 100644 --- a/src/engine/vm.zig +++ b/src/engine/vm.zig @@ -486,16 +486,15 @@ pub const Variable = enum(u8) { return @fromBackingInt(@backingInt(game)); } - /// The variable a mod adds that it is, if it is one. + /// The variable a mod adds with its number, if there is one. pub fn added(variable: Variable) ?*const additions.variables.Added { return additions.variables.get(@backingInt(variable)); } - /// The game's name for it: none for a number without one, or whose name is a placeholder, - /// such as `_unused_2`. + /// The game's name for it: none for a number without one, or whose name is a placeholder's, + /// such as `_unused_2` (`layout.tagName`). pub fn gameName(variable: Variable) ?[]const u8 { - const name = std.enums.tagName(GameVariable, @fromBackingInt(@backingInt(variable))) orelse return null; - return if (name[0] == '_') null else name; + return layout.tagName(GameVariable, @fromBackingInt(@backingInt(variable))); } /// The name scripts know it by: the qualified name of one a mod adds, else the game's name diff --git a/src/engine/web.zig b/src/engine/web.zig index d80f010e..94acdad1 100644 --- a/src/engine/web.zig +++ b/src/engine/web.zig @@ -585,8 +585,8 @@ const testing = struct { } }; - /// The quiet time the test gives `Trickles`, many times its gap, so that a busy machine can hold - /// its thread up for most of a quarter of a second without failing the test + /// The quiet time the test gives `Trickles`, many times its gap, so that a busy machine can + /// hold its thread up for most of a quarter of a second without failing the test /// ([#1312](https://github.com/OpenReliant/openreliant/issues/1312)). const trickle_quiet_ms = 250; diff --git a/src/formats/dte/source/definitions.zig b/src/formats/dte/source/definitions.zig index bd6768f6..26eb427f 100644 --- a/src/formats/dte/source/definitions.zig +++ b/src/formats/dte/source/definitions.zig @@ -10,6 +10,7 @@ const Allocator = std.mem.Allocator; const Writer = std.Io.Writer; const dte = @import("../../dte.zig"); +const layout = @import("../../layout.zig"); const commands = @import("../../../engine/game/executor/commands.zig"); const conditions = @import("../../../engine/vm/conditions.zig"); const vm = @import("../../../engine/vm.zig"); @@ -87,13 +88,13 @@ pub fn write(arena: Allocator, w: *Writer) Error!void { for (commands.table) |command| try writeCommand(arena, w, command); } -/// Writes the type `name`: the names `tags` give, but placeholders (`vocabulary.isPlaceholder`), +/// Writes the type `name`: the names `tags` give, but placeholders (`layout.isPlaceholder`), /// or a number for a value without a name, and any text where `text` is set /// (`vocabulary.Kind.hasModNames`). fn writeNames(w: *Writer, name: []const u8, tags: []const [:0]const u8, text: bool) Writer.Error!void { try w.print("export type {s} = ", .{name}); for (tags) |tag| { - if (!vocabulary.isPlaceholder(tag)) try w.print("\"{s}\" | ", .{tag}); + if (!layout.isPlaceholder(tag)) try w.print("\"{s}\" | ", .{tag}); } try w.writeAll(if (text) "number | string\n" else "number\n"); } diff --git a/src/formats/dte/source/reference.zig b/src/formats/dte/source/reference.zig index 58af1c78..f0e71909 100644 --- a/src/formats/dte/source/reference.zig +++ b/src/formats/dte/source/reference.zig @@ -9,6 +9,7 @@ const Allocator = std.mem.Allocator; const Writer = std.Io.Writer; const dte = @import("../../dte.zig"); +const layout = @import("../../layout.zig"); const commands = @import("../../../engine/game/executor/commands.zig"); const conditions = @import("../../../engine/vm/conditions.zig"); const vm = @import("../../../engine/vm.zig"); @@ -196,11 +197,11 @@ fn writeNames(w: *Writer) Writer.Error!void { try w.writeAll(", or a number, as in `game[37]`, for one without a name.\n"); } -/// Writes the names `tags` give, but placeholders (`vocabulary.isPlaceholder`), each in backticks. +/// Writes the names `tags` give, but placeholders (`layout.isPlaceholder`), each in backticks. fn writeList(w: *Writer, tags: []const [:0]const u8) Writer.Error!void { var first = true; for (tags) |tag| { - if (vocabulary.isPlaceholder(tag)) continue; + if (layout.isPlaceholder(tag)) continue; try w.print("{s}`{s}`", .{ if (first) "" else ", ", tag }); first = false; } diff --git a/src/formats/dte/source/vocabulary.zig b/src/formats/dte/source/vocabulary.zig index 5a859fce..c1290087 100644 --- a/src/formats/dte/source/vocabulary.zig +++ b/src/formats/dte/source/vocabulary.zig @@ -7,6 +7,7 @@ const std = @import("std"); const dte = @import("../../dte.zig"); +const layout = @import("../../layout.zig"); const commands = @import("../../../engine/game/executor/commands.zig"); const environfx = @import("../../../engine/game/environfx.zig"); const gameobj = @import("../../../engine/game/gameobj.zig"); @@ -95,26 +96,20 @@ pub fn Names(comptime kind: Kind) ?type { }; } -/// Whether `tag` is a placeholder for a value not understood yet, such as `_unused_2`. Mission -/// source writes such a value as a number, never by the placeholder's name. -pub fn isPlaceholder(tag: []const u8) bool { - return tag.len > 0 and tag[0] == '_'; -} - -/// The name of value `number` of `kind`, where the game names it. +/// The name of value `number` of `kind`, where the game names it. Mission source writes a value +/// whose name is a placeholder's, such as `_unused_2`, as a number (`layout.isPlaceholder`). pub fn name(kind: Kind, number: u32) ?[]const u8 { return switch (kind) { inline else => |known| if (comptime Names(known)) |Enum| { const Backing = @typeInfo(Enum).@"enum".tag_type; - const found = std.enums.tagName(Enum, @fromBackingInt(std.math.cast(Backing, number) orelse return null)) orelse return null; - return if (isPlaceholder(found)) null else found; + return layout.tagName(Enum, @fromBackingInt(std.math.cast(Backing, number) orelse return null)); } else null, }; } /// The value of `kind` that the game names `text`, the inverse of `name`. pub fn value(kind: Kind, text: []const u8) ?u32 { - if (isPlaceholder(text)) return null; + if (layout.isPlaceholder(text)) return null; return switch (kind) { inline else => |known| if (comptime Names(known)) |Enum| { return std.math.cast(u32, @backingInt(std.meta.stringToEnum(Enum, text) orelse return null)); @@ -130,7 +125,7 @@ pub fn variable(number: u8) ?[]const u8 { /// The number of the game's variable named `text`, the inverse of `variable`. pub fn variableNumber(text: []const u8) ?u8 { - if (isPlaceholder(text)) return null; + if (layout.isPlaceholder(text)) return null; return @backingInt(std.meta.stringToEnum(vm.GameVariable, text) orelse return null); } @@ -182,12 +177,6 @@ test value { try std.testing.expectEqual(null, value(.part, "start")); } -test isPlaceholder { - try std.testing.expect(isPlaceholder("_unused_2")); - try std.testing.expect(!isPlaceholder("grendel")); - try std.testing.expect(!isPlaceholder("")); -} - test variable { try std.testing.expectEqualStrings("backup_available", variable(vm.Variables.number("backup_available")).?); try std.testing.expectEqual(null, variable(vm.Variables.number("_unused_2"))); diff --git a/src/formats/layout.zig b/src/formats/layout.zig index bf73635c..3051fa1a 100644 --- a/src/formats/layout.zig +++ b/src/formats/layout.zig @@ -60,6 +60,32 @@ pub fn formatTagAs(comptime T: type, value: T, comptime noun: []const u8, writer }; } +/// Whether `tag` is a placeholder's name, for a value or a field not understood yet, such as +/// `_unused_2`: one that starts with an underscore. Scripts and mission source never name one. +pub fn isPlaceholder(tag: []const u8) bool { + return tag.len > 0 and tag[0] == '_'; +} + +/// The name of `value`'s tag, where the enum names it and the name isn't a placeholder's +/// (`isPlaceholder`). +pub fn tagName(comptime T: type, value: T) ?[]const u8 { + const name = std.enums.tagName(T, value) orelse return null; + return if (isPlaceholder(name)) null else name; +} + +test isPlaceholder { + try std.testing.expect(isPlaceholder("_unused_2")); + try std.testing.expect(!isPlaceholder("grendel")); + try std.testing.expect(!isPlaceholder("")); +} + +test tagName { + const Named = enum(u8) { grendel = 1, _unused_2 = 2, _ }; + try std.testing.expectEqualStrings("grendel", tagName(Named, .grendel).?); + try std.testing.expectEqual(null, tagName(Named, ._unused_2)); + try std.testing.expectEqual(null, tagName(Named, @fromBackingInt(3))); +} + /// An integer stored big-endian, as a field of an `extern struct`. pub fn Big(comptime T: type) type { return extern struct { diff --git a/src/formats/spr.zig b/src/formats/spr.zig index e5b20862..ae71991b 100644 --- a/src/formats/spr.zig +++ b/src/formats/spr.zig @@ -209,7 +209,8 @@ pub const Sprite = struct { /// Whether block `index` is a shape whose rows run past its block, as a broken set from a mod /// can hold: a block `classify` takes for a placeholder, but whose header's bounds are sane, - /// which the game's own placeholders' aren't. One of 256 bytes reads as a remap table instead. + /// unlike those of the game's own placeholders. A block of 256 bytes is read as a remap table + /// instead, so it is never one. pub fn cutShort(sprite: Sprite, index: usize) bool { if (sprite.block(index) != .placeholder) return false; return saneHeader(sprite.data, sprite.entries[index].offset, sprite.blockEnd(index)) != null; diff --git a/src/openreliant/main.zig b/src/openreliant/main.zig index 84dc6b93..468a02f0 100644 --- a/src/openreliant/main.zig +++ b/src/openreliant/main.zig @@ -2577,9 +2577,10 @@ const Display = struct { fn drawOverlay(display: *Display) !void { if (display.console) |console| if (console.isUp()) return console.draw(display.device, display.screen, display.strings, .mission); if (display.pause_menu.isOpen()) { - try display.pause_menu.draw(display.pauseFrame()); + const frame = display.pauseFrame(); + try display.pause_menu.draw(frame); if (display.presentation) |scripts| try scripts.draw(.ui, display.device, null); - return display.pause_menu.drawPointer(display.pauseFrame()); + return display.pause_menu.drawPointer(frame); } try display.drawDisplay(); if (display.presentation) |scripts| { diff --git a/src/openreliant/rooms.zig b/src/openreliant/rooms.zig index 9831dfd1..3e899eea 100644 --- a/src/openreliant/rooms.zig +++ b/src/openreliant/rooms.zig @@ -1060,13 +1060,16 @@ const Restarting = struct { shapes: ?canvas.Shapes = null, fn draw(screen: *Restarting, target: canvas.Canvas) canvas.Error!void { - const shapes = if (screen.shapes) |*loaded| &loaded.art else return; - try screen.state.draw(target, shapes); + try screen.state.draw(target, screen.art() orelse return); } fn drawPointer(screen: *Restarting, target: canvas.Canvas, pointer: canvas.Pointer) canvas.Error!void { - const shapes = if (screen.shapes) |*loaded| &loaded.art else return; - try screen.state.drawPointer(target, shapes, pointer); + try screen.state.drawPointer(target, screen.art() orelse return, pointer); + } + + /// The shapes the screen draws with; null where they are left out. + fn art(screen: *Restarting) ?*game.hud.Art { + return if (screen.shapes) |*loaded| &loaded.art else null; } }; diff --git a/src/scripting/records/game_variables.zig b/src/scripting/records/game_variables.zig index a0a5b80a..9c913a4a 100644 --- a/src/scripting/records/game_variables.zig +++ b/src/scripting/records/game_variables.zig @@ -1,10 +1,10 @@ //! The game's variables as scripts see them in `openreliant.records.game_variables` -//! ([#1322](https://github.com/OpenReliant/openreliant/issues/1322)): one for each number a mission's -//! script names, from 0, each a proxy for its `gameflow.VariableRecord`, whose fields load scripts -//! change in place, as they do a record's. Scripts also find a variable by its name: the game's, -//! such as `krasnaya_alive`, or the qualified name of one a mod adds. The campaign goes by them: -//! the value a new campaign gives each variable, the value each attempt at a mission starts it at, -//! and whether a saved game keeps it. +//! ([#1322](https://github.com/OpenReliant/openreliant/issues/1322)): one for each number a +//! mission's script names, from 0, each a proxy for its `gameflow.VariableRecord`, whose fields +//! load scripts change in place, as they do a record's. Scripts also find a variable by its name: +//! the game's, such as `krasnaya_alive`, or the qualified name of one a mod adds. The campaign goes +//! by them: the value a new campaign gives each variable, the value each attempt at a mission +//! starts it at, and whether a saved game keeps it. const std = @import("std"); diff --git a/src/scripting/reference.zig b/src/scripting/reference.zig index 53c95df3..05a87aeb 100644 --- a/src/scripting/reference.zig +++ b/src/scripting/reference.zig @@ -385,7 +385,8 @@ pub fn writeDefinitions(w: *Writer) Writer.Error!void { try w.writeAll(" --- The colour of the light the planets get, nil for the key lights' colour, (1, 1, 0.8), as the original lights them. Only the planets and their atmospheres take this light, so a mod can light them without lighting any other model. Load scripts set it.\n"); try w.writeAll(" planet_light: vector?,\n"); inline for (field_tables) |Table| { - // A table whose entries scripts also find by name takes strings as keys too (`table.Table`). + // A table whose entries scripts also find by name takes strings as keys too + // (`table.Table`). const key = if (@hasDecl(Table, "placeNamed")) "number | string" else "number"; try w.print(" --- {s}\n {s}: {{ [{s}]: {s} }},\n", .{ Table.entries_about, Table.list_name, key, Table.script_name }); } diff --git a/src/scripting/values.zig b/src/scripting/values.zig index ebafa050..f93feed5 100644 --- a/src/scripting/values.zig +++ b/src/scripting/values.zig @@ -384,10 +384,10 @@ pub fn lookUp(state: *State, comptime T: type, text: []const u8) ?T { return byName(T, ownName(state, text, &buffer) orelse return null); } -/// Whether scripts see a tag or a field by its name: all but those that start with an underscore, -/// which hold what isn't understood yet. +/// Whether scripts see a tag or a field by its name: all but the placeholders' names, which start +/// with an underscore and hold what isn't understood yet (`layout.isPlaceholder`). pub fn shown(tag_name: []const u8) bool { - return tag_name.len > 0 and tag_name[0] != '_'; + return tag_name.len > 0 and !openreliant.layout.isPlaceholder(tag_name); } /// The names of `T`'s values that scripts see.