Repository navigation
Expand file tree
/
Copy pathrunning.zig
More file actions
393 lines (358 loc) · 17.1 KB
/
Copy pathrunning.zig
File metadata and controls
393 lines (358 loc) · 17.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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
//! What the game's scripts and the presentation's share
//! ([#498](https://github.com/OpenReliant/openreliant/issues/498)): the scripts that run, kept in
//! lists (`Runner`): starting them, calling their engine handlers, delivering their events, and
//! stopping them.
//!
//! A script that stops while engine handlers are being called is only marked, and leaves its list
//! once the calls are done (`Runner.sweep`), so no list changes under a call. As the last of a
//! mod's scripts in one context stops, the context closes, and the hooks and interfaces its scripts
//! added go with it.
const std = @import("std");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.scripts);
const luau = @import("luau.zig");
const State = luau.State;
const script = @import("script.zig");
const runtime = @import("runtime.zig");
const Runtime = runtime.Runtime;
const Context = runtime.Context;
const hooks = @import("hooks.zig");
const values = @import("values.zig");
const data = @import("data.zig");
const events = @import("events.zig");
const interfaces = @import("interfaces.zig");
const async_module = @import("async.zig");
const Name = runtime.Name;
/// A script that runs.
pub const Running = struct {
context: *Context,
/// Its file's name, as its mod has it.
name: []const u8,
offered: runtime.Offered,
/// Whether it's a mission's, which stops as its mission ends.
mission: bool = false,
/// Whether it has stopped, and waits for the calls walking its list to finish (`Runner.sweep`).
stopped: bool = false,
/// How many calls in a row of each of its engine handlers ran out of time.
timeouts: std.EnumArray(script.Handler, runtime.Timeouts) = .initFill(.{}),
};
/// The scripts that run in one place, in the order they started.
pub const List = std.ArrayList(Running);
/// A timer waiting to run the function its mod registered under `name` (`async.zig`).
pub const Timer = struct {
context: *Context,
name: Name,
/// The seconds left until it runs.
left: f64,
/// What it passes the function, held in the state; null for nil.
data: ?luau.Ref,
/// The round of `Runner.advance` it was started in, which runs only those started before it.
round: u32 = 0,
};
/// The scripts of one side, in their lists, with their events and interfaces.
pub const Runner = struct {
gpa: Allocator,
runtime: *Runtime,
/// Every list of scripts of the side, which a sweep goes through.
lists: []List,
events: events.Events,
interfaces: interfaces.Interfaces,
/// The hooks the scripts add handlers to; null for the presentation side, which has none yet.
hooks: ?*hooks.Hooks = null,
/// The timers waiting, in the order they were started.
timers: std.ArrayList(Timer) = .empty,
/// How many times `advance` has run, which tells the timers it runs from those started since.
round: u32 = 0,
/// How many calls walk the lists, whose stopped scripts wait for them.
walking: u32 = 0,
/// Whether a script stopped while calls walked the lists.
stopped: bool = false,
pub fn init(gpa: Allocator, scripts: *Runtime, lists: []List) Runner {
return .{
.gpa = gpa,
.runtime = scripts,
.lists = lists,
.events = .{ .gpa = gpa, .runtime = scripts },
.interfaces = .{ .gpa = gpa, .runtime = scripts },
};
}
/// Stops every script and lets go of the lists.
pub fn deinit(runner: *Runner) void {
for (runner.lists) |*list| runner.stopAll(list);
for (runner.lists) |*list| list.deinit(runner.gpa);
for (runner.timers.items) |timer| if (timer.data) |ref| runner.runtime.release(ref);
runner.timers.deinit(runner.gpa);
runner.events.deinit();
runner.interfaces.deinit();
}
/// Adds `timer`, which then holds its data's reference.
pub fn addTimer(runner: *Runner, timer: Timer) error{ OutOfMemory, TooMany }!void {
if (runner.timers.items.len == async_module.max_timers) return error.TooMany;
var started = timer;
started.round = runner.round;
try runner.timers.append(runner.gpa, started);
}
/// Moves the timers on by `seconds`, and runs those whose time has come, soonest first, each
/// once. Timers started meanwhile wait for the next.
pub fn advance(runner: *Runner, seconds: f64) void {
for (runner.timers.items) |*timer| timer.left -= seconds;
// The timers this call's functions start belong to the round after it.
runner.round +%= 1;
while (true) {
var soonest: ?usize = null;
for (runner.timers.items, 0..) |timer, at| {
if (timer.round == runner.round or timer.left > 0) continue;
if (soonest == null or timer.left < runner.timers.items[soonest.?].left) soonest = at;
}
const at = soonest orelse return;
runner.fire(runner.timers.orderedRemove(at));
}
}
/// Runs the function `timer` names, and lets go of its data.
fn fire(runner: *Runner, timer: Timer) void {
defer if (timer.data) |ref| runner.runtime.release(ref);
const context = timer.context;
if (context.closed) return;
const name = timer.name.slice();
const callbacks = context.callbacks orelse return missingCallback(context, name);
runner.walking += 1;
defer runner.leave();
const called = runner.runtime.callIn(context, callbacks, name, .{timer.data}) orelse return missingCallback(context, name);
if (called.raised()) log.warn("{s}: the timer {s} failed", .{ context.modOf().name, name });
}
fn missingCallback(context: *const Context, name: []const u8) void {
log.warn("{s}: a timer names {s}, which no script registered (async.register_timer)", .{ context.modOf().name, name });
}
/// Runs the script `name` of the mod opened as `context`, keeps what it offers in `list`, and
/// calls its `on_init` with `payload` unless it's `loading`, as a saved game's scripts start
/// (`snapshot.restore`), then, for an object script, its `on_added`. Returns where it went in
/// `list`; null for a script that fails, which is logged and left out. `payload` goes with the
/// script.
pub fn start(runner: *Runner, list: *List, context: *Context, name: []const u8, mission: bool, payload: ?data.Data, loading: bool) Allocator.Error!?usize {
defer if (payload) |given| runner.runtime.release(given.ref);
const mod = context.modOf();
const returned = runner.runtime.run(context, name) orelse return null;
defer runner.runtime.release(returned);
var offered = context.offerOf(name, returned) orelse return null;
list.append(runner.gpa, .{ .context = context, .name = name, .offered = offered, .mission = mission }) catch |err| {
offered.release(runner.runtime);
return err;
};
const at = list.items.len - 1;
log.info("{s}: started {s}", .{ mod.name, name });
if (offered.interface) |interface| {
if (try runner.interfaces.offer(context, interface.name, interface.table)) |base| {
runner.callOne(list, at, .on_interface_override, .{ .base = .{ .ref = base } });
}
}
if (!loading) runner.callOne(list, at, .on_init, .{ .data = payload });
if (context.family == .object) runner.callOne(list, at, .on_added, .{});
return at;
}
/// Calls the engine handler `handler` of the script at `at` in `list` with `arguments`.
pub fn callOne(runner: *Runner, list: *List, at: usize, comptime handler: script.Handler, arguments: script.Handler.Arguments(handler)) void {
var made: Made = .{};
defer made.release(runner.runtime);
const passed = made.pass(runner.runtime, arguments) orelse return;
runner.walking += 1;
defer runner.leave();
runner.callPassed(list, at, handler, passed);
}
/// Calls `handler` of each running script of `list` with `arguments`, in order. Scripts that
/// start meanwhile are called too.
pub fn callEach(runner: *Runner, list: *List, comptime handler: script.Handler, arguments: script.Handler.Arguments(handler)) void {
runner.callEachOf(list, null, handler, arguments);
}
/// `callEach`, for the scripts of the mod called `mod` only; of every mod where it is null.
fn callEachOf(runner: *Runner, list: *List, mod: ?[]const u8, comptime handler: script.Handler, arguments: script.Handler.Arguments(handler)) void {
if (list.items.len == 0) return;
var made: Made = .{};
defer made.release(runner.runtime);
const passed = made.pass(runner.runtime, arguments) orelse return;
runner.walking += 1;
defer runner.leave();
var at: usize = 0;
while (at < list.items.len) : (at += 1) {
if (mod) |wanted| if (!std.mem.eql(u8, list.items[at].context.modOf().name, wanted)) continue;
runner.callPassed(list, at, handler, passed);
}
}
/// `callAll`, for the scripts of the mod called `mod` only.
pub fn callMod(runner: *Runner, mod: []const u8, comptime handler: script.Handler, arguments: script.Handler.Arguments(handler)) void {
runner.walking += 1;
defer runner.leave();
for (runner.lists) |*list| runner.callEachOf(list, mod, handler, arguments);
}
/// Whether a running script of any list has `handler`.
pub fn offers(runner: *const Runner, comptime handler: script.Handler) bool {
for (runner.lists) |list| for (list.items) |held| {
if (!held.stopped and held.offered.handlers.get(handler) != null) return true;
};
return false;
}
/// `callEach`, for every list in order.
pub fn callAll(runner: *Runner, comptime handler: script.Handler, arguments: script.Handler.Arguments(handler)) void {
runner.walking += 1;
defer runner.leave();
for (runner.lists) |*list| runner.callEach(list, handler, arguments);
}
/// Calls `handler` of the script at `at` in `list`, and lets go of it where the call failed,
/// or ran out of time too many times in a row (`runtime.Timeouts`).
fn callPassed(runner: *Runner, list: *List, at: usize, comptime handler: script.Handler, passed: anytype) void {
const running = &list.items[at];
if (running.stopped) return;
const function = running.offered.handlers.get(handler) orelse return;
const called = runner.runtime.call(running.context, function, passed);
// The call may have added scripts to the list, which moves it, or stopped this one, which
// let go of its handlers.
const held = &list.items[at];
if (!held.timeouts.getPtr(handler).removes(called) or held.offered.handlers.get(handler) != function) return;
log.warn("{s}: {s}: {t} {s}, and isn't called again", .{ held.context.modOf().name, held.name, handler, runtime.Timeouts.why(called) });
runner.runtime.release(function);
held.offered.handlers.set(handler, null);
}
/// Ends a walk of the lists, sweeping them once the last walk is done.
pub fn leave(runner: *Runner) void {
runner.walking -= 1;
if (runner.walking == 0 and runner.stopped) runner.sweep();
}
/// Stops the script at `at` in `list`: lets go of what it offered, and closes its mod's context
/// once none of its scripts there runs (`closeUnused`). It leaves the list once no call walks
/// it (`sweep`).
pub fn stopScript(runner: *Runner, list: *List, at: usize) void {
const running = &list.items[at];
if (running.stopped) return;
running.stopped = true;
running.offered.release(runner.runtime);
runner.closeUnused(list, running.context);
runner.stopped = true;
if (runner.walking == 0) runner.sweep();
}
/// Closes `context` where none of its scripts in `list` runs: after its last script there
/// stops, or where none of the scripts it was opened for started.
pub fn closeUnused(runner: *Runner, list: *const List, context: *Context) void {
for (list.items) |*other| {
if (!other.stopped and other.context == context) return;
}
runner.closeContext(context);
}
/// Stops every script of `list`.
pub fn stopAll(runner: *Runner, list: *List) void {
runner.walking += 1;
defer runner.leave();
for (0..list.items.len) |at| runner.stopScript(list, at);
}
/// Closes a mod opened for scripts that have all stopped: their hooks and interfaces go.
pub fn closeContext(runner: *Runner, context: *Context) void {
if (runner.hooks) |held| held.removeContext(context);
runner.interfaces.removeContext(context);
var kept: usize = 0;
for (runner.timers.items) |timer| {
if (timer.context == context) {
if (timer.data) |ref| runner.runtime.release(ref);
continue;
}
runner.timers.items[kept] = timer;
kept += 1;
}
runner.timers.shrinkRetainingCapacity(kept);
runner.runtime.close(context);
}
/// Takes the stopped scripts out of their lists.
fn sweep(runner: *Runner) void {
for (runner.lists) |*list| {
var kept: usize = 0;
for (list.items) |running| {
if (running.stopped) continue;
list.items[kept] = running;
kept += 1;
}
list.shrinkRetainingCapacity(kept);
}
runner.stopped = false;
}
/// Calls the handlers of `list` for the event `sent`, newest mod first, and within a mod in the
/// order its scripts started.
pub fn deliverTo(runner: *Runner, list: *List, sent: *const events.Pending) void {
runner.walking += 1;
defer runner.leave();
const name = sent.name.slice();
var mod = runner.runtime.mods.len;
while (mod > 0) {
mod -= 1;
var at: usize = 0;
while (at < list.items.len) : (at += 1) {
const running = &list.items[at];
if (running.stopped or running.context.mod != mod) continue;
const table = running.offered.event_handlers orelse continue;
const called = runner.runtime.callIn(running.context, table, name, .{sent.data.ref}) orelse continue;
switch (called) {
.returned_false => return,
.failed, .timed_out => log.warn("{s}: {s}: the handler of the event {s} failed", .{ list.items[at].context.modOf().name, list.items[at].name, name }),
.returned, .returned_nil => {},
}
}
}
}
};
/// The values made to pass an engine handler its arguments, let go once it has been called.
const Made = struct {
refs: [max_made]luau.Ref = undefined,
len: usize = 0,
/// The most arguments a handler takes that are made rather than pushed as they are.
const max_made = 4;
fn release(made: *Made, scripts: *Runtime) void {
for (made.refs[0..made.len]) |ref| scripts.release(ref);
}
/// The arguments `Runtime.call` takes for `arguments`: numbers as they are, and a reference to
/// each value made for the others. Null if one can't be made.
fn pass(made: *Made, scripts: *Runtime, arguments: anytype) ?Passed(@TypeOf(arguments)) {
const Arguments = @TypeOf(arguments);
var passed: Passed(Arguments) = undefined;
const info = @typeInfo(Arguments).@"struct";
inline for (info.field_names, info.field_types, 0..) |name, Field, at| {
const value = @field(arguments, name);
passed[at] = switch (comptime passing(Field)) {
.number => value,
.optional_data => if (value) |given| given.ref else null,
.table => value.ref,
.made => ref: {
const ref = scripts.make(Push(Field).push, .{value}) orelse return null;
made.refs[made.len] = ref;
made.len += 1;
break :ref ref;
},
};
}
return passed;
}
/// How an argument of `T` is passed.
const Passing = enum { number, optional_data, table, made };
fn passing(comptime T: type) Passing {
if (T == ?data.Data) return .optional_data;
if (T == values.Table) return .table;
return switch (@typeInfo(T)) {
.float, .int => .number,
else => .made,
};
}
/// The tuple `pass` gives for a handler's `Arguments`.
fn Passed(comptime Arguments: type) type {
const fields = @typeInfo(Arguments).@"struct".field_types;
var types: [fields.len]type = undefined;
for (&types, fields) |*passed, Field| passed.* = switch (passing(Field)) {
.number => Field,
.optional_data => ?luau.Ref,
.table, .made => luau.Ref,
};
return @Tuple(&types);
}
/// What pushes a value of `T` (`Runtime.make`).
fn Push(comptime T: type) type {
return struct {
fn push(state: *State, value: T) void {
values.push(state, T, value);
}
};
}
};