Repository navigation
Expand file tree
/
Copy pathlibrary.zig
More file actions
294 lines (267 loc) · 14.1 KB
/
Copy pathlibrary.zig
File metadata and controls
294 lines (267 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
//! The models objects are made of, read from the game's archive once each and kept while they are
//! used: each ship type's, which `ship_type_load` (`0x00466740`) loads for the type's first object
//! with its schematic (`TypeCache`), and the models the types' attachment points mount
//! (`MountCache`). They answer `create.Types` and `objects.Mounts`, which the tests answer with
//! models of their own.
const std = @import("std");
const Allocator = std.mem.Allocator;
const spr = @import("../../../formats/spr.zig");
const device = @import("../../surrender/srd3d/device.zig");
const srtexture = @import("../../surrender/surrenderlib/srtexture.zig");
const bigfile = @import("../bigfile.zig");
const create = @import("../create.zig");
const additions = @import("../additions.zig");
const hud = @import("../hud.zig");
const objects = @import("../objects.zig");
const srofiles = @import("../srofiles.zig");
const main = @import("../main.zig");
const log = std.log.scoped(.library);
/// The models attachment points mount, by file, each read the first time it is asked for and kept
/// for whoever mounts it: a ship of three of the same turret reads that turret once. A model the
/// game lacks is remembered as missing, so it is looked for only once. Everything is made in `gpa`,
/// an arena, such as a ship type's own, so that letting the type go lets the lot go.
pub const MountCache = struct {
gpa: Allocator,
resources: *const bigfile.Hog,
textures: *srtexture.Table,
/// What the models are built with: the light maps (`Lmaps`) and the lights.
models: srofiles.Settings = .{},
read: std.StringHashMapUnmanaged(?objects.Mounts.Mounted) = .empty,
pub fn mounts(cache: *MountCache) objects.Mounts {
return .{ .context = cache, .load = load };
}
fn load(context: *anyopaque, file: []const u8) ?objects.Mounts.Mounted {
const cache: *MountCache = @ptrCast(@alignCast(context));
if (cache.read.get(file)) |found| return found;
const mounted = srofiles.readModel(cache.gpa, cache.resources, cache.textures, file, cache.models) catch |err| missing: {
log.warn("the model {s} is not mounted: {s}", .{ file, @errorName(err) });
break :missing null;
};
cache.read.put(cache.gpa, file, mounted) catch return mounted;
return mounted;
}
};
/// Where a ship type's schematic comes from: the sprite set its shapes are read from, and the set
/// name the pictures that replace them are named after (`hud.Art.Pictures`).
const Schematic = struct {
layout: []const u8,
pictures: []const u8,
};
/// The schematic named `named` of ship type `ship_type`. A type a mod adds whose schematic no file
/// holds takes its shapes from its base's set, and the mod's pictures named after its own schematic
/// draw over them: `teapotscem_000.png` over the Predator's schematic for a type based on it that
/// names `teapotscem.spr`. A mod can so give its type a schematic without writing a sprite set.
fn schematicOf(resources: *const bigfile.Hog, ship_type: create.TypeIndex, named: ?[]const u8) ?Schematic {
const own = named orelse return null;
const added = additions.ships.get(ship_type) orelse return .{ .layout = own, .pictures = own };
if (resources.has(own)) return .{ .layout = own, .pictures = own };
const base = create.models.ship_types[@backingInt(added.base)].schematic orelse return null;
return .{ .layout = base, .pictures = own };
}
/// The ship types' models (`ship_type_load`), each read as `create_object` asks for it, with what
/// it mounts and its schematic, in an arena of its own that `sweep` lets go once no object is of
/// the type.
pub const TypeCache = struct {
gpa: Allocator,
resources: *const bigfile.Hog,
textures: *srtexture.Table,
/// What the models are built with, the cockpit's among them: the light maps (`Lmaps`) and the
/// lights.
models: srofiles.Settings = .{},
/// What the types' objects light themselves with; each type mounts from its own `MountCache`.
looks: objects.Effects,
/// VFX's global palette, which the schematics are drawn with.
global_palette: ?*const [spr.palette_size]u8,
/// The display's shapes (`hud.hardware_shapes`), which a mod's type's own wire frame and wing
/// icon draw over (`ownShapes`); none without them.
display_shapes: ?spr.Sprite = null,
loaded: [create.max_ship_types]?*Cached = @splat(null),
/// Types the game names no model for, or whose files it lacks, looked for once.
missing: std.bit_set.Static(create.max_ship_types) = .empty,
const Cached = struct {
arena: std.heap.ArenaAllocator,
type: create.Type,
mounted: MountCache,
/// The schematic the display's ship status indicator draws, where the game has one.
schematic: ?hud.Art,
/// The display's shapes with a mod's type's own pictures (`ownShapes`).
wire_frame: ?hud.Art,
wing_icon: ?hud.Art,
};
pub fn types(cache: *TypeCache) create.Types {
return .{ .context = cache, .load = load };
}
/// The type's model, loaded the first time; null for a type the game names no model for, and
/// for one whose model it lacks or can't read, which is logged.
fn load(context: *anyopaque, ship_type: create.TypeIndex) ?*const create.Type {
const cache: *TypeCache = @ptrCast(@alignCast(context));
if (cache.loaded[ship_type]) |cached| return &cached.type;
if (cache.missing.isSet(ship_type)) return null;
const files = create.shipFiles(ship_type);
const name = files.model orelse {
cache.missing.set(ship_type);
return null;
};
const cached = cache.build(ship_type, name, schematicOf(cache.resources, ship_type, files.schematic)) catch |err| {
log.warn("ship type {d} has no model: {s}", .{ ship_type, @errorName(err) });
cache.missing.set(ship_type);
return null;
};
cache.loaded[ship_type] = cached;
return &cached.type;
}
fn build(cache: *TypeCache, ship_type: create.TypeIndex, name: []const u8, schematic: ?Schematic) !*Cached {
const cached = try cache.gpa.create(Cached);
errdefer cache.gpa.destroy(cached);
cached.arena = .init(cache.gpa);
errdefer cached.arena.deinit();
const gpa = cached.arena.allocator();
const file = try srofiles.readModel(gpa, cache.resources, cache.textures, name, cache.models);
cached.mounted = .{ .gpa = gpa, .resources = cache.resources, .textures = cache.textures, .models = cache.models };
cached.schematic = if (schematic) |files| found: {
const bytes = cache.resources.readFile(gpa, files.layout) catch |err| {
log.warn("the schematic {s} is left out: {s}", .{ files.layout, @errorName(err) });
break :found null;
};
const set = try spr.Sprite.parse(bytes);
hud.warnCutShort(set, files.layout);
break :found try .init(gpa, set, cache.global_palette, .of(cache.resources.mods, files.pictures));
} else null;
cached.wire_frame = try cache.ownShapes(gpa, ship_type, .wire_frame);
cached.wing_icon = try cache.ownShapes(gpa, ship_type, .wing_icon);
var effects = cache.looks;
effects.mounts = cached.mounted.mounts();
cached.type = .{
.model = file.model,
.loaded = file.loaded,
.effects = effects,
.schematic = if (cached.schematic) |*art| .{ .art = art, .gpa = gpa } else null,
.wire_frame = if (cached.wire_frame) |*art| .{ .art = art, .gpa = gpa } else null,
.wing_icon = if (cached.wing_icon) |*art| .{ .art = art, .gpa = gpa } else null,
};
return cached;
}
/// The display's shapes, with the pictures a mod's ship type gives in place of its base's
/// `which` (`additions.ShipExtra`): the picture numbered 0 stands for the shape the type is
/// flown with (`main.PlayerShip`), its base's, and for the wire frame, those after it for the
/// shapes after it, the groups of guns lit, each drawn over the whole frame. Null for a type
/// that gives none, or isn't flown (`main.playerShip`).
fn ownShapes(cache: *const TypeCache, gpa: std.mem.Allocator, ship_type: create.TypeIndex, comptime which: enum { wire_frame, wing_icon }) std.mem.Allocator.Error!?hud.Art {
const added = additions.ships.get(ship_type) orelse return null;
const set = @field(added.extra, @tagName(which)) orelse return null;
const shapes = cache.display_shapes orelse return null;
const player = main.playerShip(@fromBackingInt(ship_type)) orelse return null;
const pictures: hud.Art.Pictures = .{
.files = cache.resources.mods.pictures(),
.set = set,
.first = switch (which) {
.wire_frame => player.wire_frame orelse return null,
.wing_icon => player.wing_icon,
},
.over_first = which == .wire_frame,
};
return try .init(gpa, shapes, cache.global_palette, pictures);
}
/// Lets go of each type no object is of any more, by the objects' count of each (`uses`), except
/// those kept for drawing (`kept`).
pub fn sweep(cache: *TypeCache, uses: *const [create.max_ship_types]create.TypeUse, kept: *const create.TypeSet) void {
for (&cache.loaded, uses, 0..) |*held, use, index| {
const cached = held.* orelse continue;
if (use.objects > 0 or kept.isSet(index)) continue;
cache.free(cached);
held.* = null;
}
}
pub fn deinit(cache: *TypeCache) void {
for (cache.loaded) |held| if (held) |cached| cache.free(cached);
}
fn free(cache: *TypeCache, cached: *Cached) void {
// The type's own shapes go first, so that their textures go back to the device.
const gpa = cached.arena.allocator();
for ([_]*?hud.Art{ &cached.schematic, &cached.wire_frame, &cached.wing_icon }) |art| if (art.*) |*made| made.deinit(gpa);
cached.arena.deinit();
cache.gpa.destroy(cached);
}
};
/// Files for the tests of what reads models.
pub const testing = struct {
pub const Files = TestFiles;
};
/// An archive of one model, `file`, whose material is `Yank_1`, in a directory of its own, and a
/// texture table holding that texture.
const TestFiles = struct {
tmp: std.testing.TmpDir,
resources: bigfile.Hog,
textures: *srtexture.testing.Textures,
pub fn init(gpa: Allocator, file: []const u8) !TestFiles {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{});
errdefer tmp.cleanup();
var buffer: [4096]u8 = undefined;
try bigfile.testing.write(gpa, io, tmp.dir, bigfile.resource_name, &.{.{ .name = file, .data = @import("../../../formats/shp.zig").testing.buildModel(&buffer) }});
var resources: bigfile.Hog = try .open(gpa, io, tmp.dir, bigfile.resource_name);
errdefer resources.close(gpa);
return .{ .tmp = tmp, .resources = resources, .textures = try .init(gpa, &.{ "yank_1", "lyank_1", "cloak64" }) };
}
pub fn deinit(files: *TestFiles, gpa: Allocator) void {
files.textures.deinit(gpa);
files.resources.close(gpa);
files.tmp.cleanup();
}
};
test MountCache {
const gpa = std.testing.allocator;
var files: TestFiles = try .init(gpa, "Gun.SHP");
defer files.deinit(gpa);
var arena: std.heap.ArenaAllocator = .init(gpa);
defer arena.deinit();
var cache: MountCache = .{ .gpa = arena.allocator(), .resources = &files.resources, .textures = &files.textures.table };
const mounts = cache.mounts();
// Read once, then kept.
const gun = mounts.load(mounts.context, "Gun.SHP").?;
try std.testing.expectEqual(gun.model, mounts.load(mounts.context, "Gun.SHP").?.model);
try std.testing.expectEqual(1, cache.read.count());
}
test TypeCache {
const gpa = std.testing.allocator;
// The torpedo's model, type 74, which has no schematic.
const torpedo = 74;
var files: TestFiles = try .init(gpa, create.models.ship_types[torpedo].model.?);
defer files.deinit(gpa);
var cache: TypeCache = .{ .gpa = gpa, .resources = &files.resources, .textures = &files.textures.table, .looks = .{}, .global_palette = null };
defer cache.deinit();
const types = cache.types();
const loaded = types.load(types.context, torpedo).?;
try std.testing.expectEqual(1, loaded.model.parts.len);
try std.testing.expect(loaded.effects.mounts != null);
try std.testing.expectEqual(null, loaded.schematic);
try std.testing.expectEqual(loaded, types.load(types.context, torpedo).?);
// A type the game names no model for has none.
const modelless = 14;
try std.testing.expectEqual(null, create.models.ship_types[modelless].model);
try std.testing.expectEqual(null, types.load(types.context, modelless));
try std.testing.expect(cache.missing.isSet(modelless));
// A schematic of its own, drawn, which the device makes a texture of.
const cached = cache.loaded[torpedo].?;
const arena = cached.arena.allocator();
cached.schematic = try .init(arena, try .parse(try spr.testing.paletteAndShape(arena)), null, null);
var recorder: device.testing.Recorder = .{ .gpa = gpa, .textures = .{} };
defer recorder.deinit();
try hud.drawShape(&cached.schematic.?, arena, recorder.interface(), 1, .{ 0, 0 }, .{ 1, 1, 1, 1 }, 1);
try std.testing.expectEqual(1, recorder.textures.?.held());
// Swept while an object is of it, or it is kept for drawing, the type stays; once neither holds,
// it goes, and its own shapes hand their textures back to the device.
var uses: [create.max_ship_types]create.TypeUse = @splat(.{});
var kept: create.TypeSet = .empty;
uses[torpedo].objects = 1;
cache.sweep(&uses, &kept);
try std.testing.expect(cache.loaded[torpedo] != null);
uses[torpedo].objects = 0;
kept.set(torpedo);
cache.sweep(&uses, &kept);
try std.testing.expect(cache.loaded[torpedo] != null);
kept.unset(torpedo);
cache.sweep(&uses, &kept);
try std.testing.expectEqual(null, cache.loaded[torpedo]);
try std.testing.expectEqual(0, recorder.textures.?.held());
}