loreline 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,24 +61,28 @@ For loreline-lua, run `--lua` first.
61
61
  Python, Lua, PHP, JS, JVM and GDScript tests require running the corresponding build step first (e.g. `node ./setup --py` before Python tests). GDScript tests also need a Godot 4 binary on the PATH (or pointed to by the `GODOT_BIN` environment variable).
62
62
 
63
63
  ```sh
64
- node run test ./test # Run Neko tests only
65
- python3 py/test-runner.py ./test # Run Python tests only
66
- lua5.4 lua/test-runner.lua ./test # Run Lua tests only
67
- php php/test-runner.php ./test # Run PHP tests only
68
- php build/php-cli/index.php test ./test # Run PHP CLI tests only
69
- npx tsx js/test-runner.ts ./test # Run JS tests only
64
+ node run test ./test --internal # Run Neko tests only
65
+ python3 py/test-runner.py ./test # Run Python tests only
66
+ lua5.4 lua/test-runner.lua ./test # Run Lua tests only
67
+ php php/test-runner.php ./test # Run PHP tests only
68
+ php build/php-cli/index.php test ./test --internal # Run PHP CLI tests only
69
+ npx tsx js/test-runner.ts ./test # Run JS tests only
70
70
  java -cp jvm/loreline.jar:build/jvm/test TestRunner ./test # Run JVM tests only
71
- node ./setup --gdscript-test # Run GDScript tests only (headless Godot)
72
- node ./setup --test # Build and run all test suites (Neko + C# + C# AOT + C# (with generics) + JS + C++ lib + Python + Lua + PHP + PHP CLI + JVM)
71
+ node ./setup --gdscript-test # Run GDScript tests only (headless Godot)
72
+ node ./setup --test # Build and run all test suites (Neko + C# + C# AOT + C# (with generics) + JS + C++ lib + Python + Lua + PHP + PHP CLI + JVM)
73
73
  ```
74
74
 
75
- ### How Loreline is authored
75
+ Each `test/*.lor` file ends with a `<test>` YAML block describing its test cases (`choices`, `beat`, `expected`, ...). `saveAtDialogue` and `saveAtChoice` take one index or a list of indices (`saveAtDialogue: [2, 5]`), counted over an uninterrupted run: at each one, the runner saves, restores in a new interpreter and continues, so the saved event shows twice in `expected`.
76
76
 
77
- Loreline's runtime is essentially handwritten, so the code you embed when depending on it can be considered almost entirely free of AI-generated code.
77
+ On top of the explicit cases, the Haxe runners (Neko, C# with generics, PHP CLI, GDScript) run a save sweep: every test case is saved and restored at each of its events (and at every pair of events in files with insertions), and must give the uninterrupted output with the saved events shown twice. Files relying on randomness are skipped, and a test case can opt out with `saveSweep: false`.
78
78
 
79
- That said, I used coding assistants for peripheral work: unit-tests, scaffolding, debugging, documenting the code, continuous integration, and help in making surgical changes to the runtime. These improve the overall reliability and quality of Loreline, make sure that anything tested won't break in the future, and give me more bandwidth for the more creative, interesting and trickier parts of Loreline itself. I weighed that this targeted use would benefit the users of the project, and I take entire responsibility for all the code and design choices in this repository.
79
+ ## How Loreline is authored
80
80
 
81
- For some context on where I stand, I don't endorse the use of generative AI as a general rule: I do care a lot about writing code myself and human authorship in general, and I also don't use any AI-generated assets or content for the games I make.
81
+ Loreline's core runtime (its lexer, parser, interpreter and the overall architecture that ties them together) has been written by hand. The Loreline language is also the result of many design iterations that are essentially human authored.
82
+
83
+ That said, I have used coding assistants for unit tests, scaffolding, debugging, documenting the code, continuous integration, fixes to the runtime and help implementing new features. All of this improves the overall reliability and quality of Loreline and makes sure that anything tested won't break in the future. I considered that this use would benefit the users of the project, and I take full responsibility for all the code and design choices in this repository.
84
+
85
+ For some context on where I stand, I don't endorse the use of generative AI as a general rule: I care a lot about writing code myself, especially when used as a creative medium, as well as human authorship in general. I reject the use of any AI-generated asset or content in the games I make.
82
86
 
83
87
  ## License
84
88
 
package/js/loreline.d.ts CHANGED
@@ -13,7 +13,7 @@ export type ImportsFileHandler = (path: string, callback: (data: string) => void
13
13
  export type ImportsErrorHandler = (error: Error) => void;
14
14
 
15
15
  /**
16
- * Tokens type
16
+ * List of tokens produced by the lexer.
17
17
  */
18
18
  export type Tokens = Array<any>;
19
19
 
@@ -24,7 +24,7 @@ export type SaveData = any;
24
24
 
25
25
  /**
26
26
  * Opaque type for a translations map.
27
- * Obtained from `Loreline.extractTranslations()` and passed to `InterpreterOptions.translations`.
27
+ * Obtained from `Loreline.loadLocale()` and passed to `InterpreterOptions.translations`.
28
28
  */
29
29
  export type Translations = any;
30
30
 
@@ -47,6 +47,17 @@ export interface Error {
47
47
  */
48
48
  stack: Array<any>;
49
49
 
50
+ /**
51
+ * The English template the message was made from, when tools can show it
52
+ * in another language. The message itself is always in English.
53
+ */
54
+ template?: string | null;
55
+
56
+ /**
57
+ * The parameters of the template, by name.
58
+ */
59
+ params?: any;
60
+
50
61
  /**
51
62
  * Converts the error to a human-readable string.
52
63
  * @return Formatted error message with position
@@ -54,6 +65,35 @@ export interface Error {
54
65
  toString(): string;
55
66
  }
56
67
 
68
+ /**
69
+ * The texts the Loreline tools show, in English or in other languages. Errors
70
+ * keep their English message for games; a tool can show them in French by
71
+ * loading `loreline-fr.js` and asking `errorText` for French.
72
+ *
73
+ * ```ts
74
+ * import { I18n } from './loreline.js';
75
+ * import fr from './loreline-fr.js';
76
+ * I18n.registerObject('fr', fr);
77
+ * const text = I18n.errorText(error, 'fr');
78
+ * ```
79
+ */
80
+ export class I18n {
81
+ /** The language of the texts: `en` or `fr`. */
82
+ static locale: string;
83
+
84
+ /** Adds the translations of a table, `{ english: translated }`, for a language. */
85
+ static registerObject(locale: string, table: { [english: string]: string }): void;
86
+
87
+ /** The message of an error in a language, from its template, or its English message. */
88
+ static errorText(error: Error, locale?: string): string;
89
+
90
+ /** A template in a language, its `{name}` parameters filled. */
91
+ static format(template: string, params?: any, locale?: string): string;
92
+
93
+ /** The language for a language tag: `fr-CA` gives `fr`, anything not French gives `en`. */
94
+ static fromTag(tag: string | null): string;
95
+ }
96
+
57
97
  /**
58
98
  * Represents a position within source code, tracking line number, column, and offset information.
59
99
  * Used throughout the compiler to pinpoint locations of tokens, nodes, and error messages.
@@ -65,14 +105,14 @@ export interface Position {
65
105
  line: number;
66
106
 
67
107
  /**
68
- * The column number in the source code, starting from 1.
69
- * Represents the character position within the current line.
108
+ * The column in the source code, starting from 1, in UTF-16 units, those
109
+ * of a JavaScript string: a character above U+FFFF counts for two.
70
110
  */
71
111
  column: number;
72
112
 
73
113
  /**
74
- * The absolute character offset from the start of the source code.
75
- * Used for precise positioning and span calculations.
114
+ * The offset from the start of the source code, in UTF-16 units: an index
115
+ * into the source as a JavaScript string.
76
116
  */
77
117
  offset: number;
78
118
 
@@ -90,7 +130,7 @@ export interface Position {
90
130
  }
91
131
 
92
132
  /**
93
- * Represents a choice option presented to the user.
133
+ * Represents a choice option presented to the player.
94
134
  */
95
135
  export interface ChoiceOption {
96
136
  /**
@@ -124,7 +164,11 @@ export interface TextTag {
124
164
  value: string;
125
165
 
126
166
  /**
127
- * The offset in the text where this tag appears.
167
+ * Where the tag appears in the text, in characters (Unicode code points)
168
+ * from its start, the same on every target. A JavaScript string counts
169
+ * UTF-16 units instead, where a character above U+FFFF, such as most emoji,
170
+ * takes two: split the text with `[...text]` to index it in characters, for
171
+ * example `[...text].slice(0, tag.offset).join('')` for the text before the tag.
128
172
  */
129
173
  offset: number;
130
174
  }
@@ -147,7 +191,7 @@ export type DialogueHandler = (interpreter: Interpreter, character: string | nul
147
191
  *
148
192
  * @param interpreter The interpreter instance
149
193
  * @param options The available choice options
150
- * @param callback Function to call with the index of the selected choice
194
+ * @param callback Function to call with the index of the selected option
151
195
  */
152
196
  export type ChoiceHandler = (interpreter: Interpreter, options: Array<ChoiceOption>, callback: (index: number) => void) => void;
153
197
 
@@ -174,15 +218,20 @@ export interface InterpreterOptions {
174
218
  strictAccess?: boolean;
175
219
 
176
220
  /**
177
- * A custom instantiator to create fields objects.
221
+ * A custom instantiator for the objects that hold fields.
178
222
  */
179
223
  customCreateFields?: (interpreter: Interpreter, type: string, node: Node) => any;
180
224
 
181
225
  /**
182
- * Optional translations map for localization.
183
- * Built from a parsed translation file using `Loreline.extractTranslations()`.
226
+ * Optional translations map for localization, as `Loreline.loadLocale()` returns it.
184
227
  */
185
228
  translations?: Translations;
229
+
230
+ /**
231
+ * If set to true, the interpreter prepares its caches when it is created,
232
+ * before the script starts, like `Interpreter.prepareCaches()`.
233
+ */
234
+ prepareCaches?: boolean;
186
235
  }
187
236
 
188
237
  /**
@@ -191,7 +240,7 @@ export interface InterpreterOptions {
191
240
  export type FunctionsMap = Record<string, Function>;
192
241
 
193
242
  /**
194
- * The main public API for Loreline runtime.
243
+ * The main public API for the Loreline runtime.
195
244
  * Provides easy access to the core functionality for parsing and running Loreline scripts.
196
245
  */
197
246
  export class Loreline {
@@ -203,7 +252,7 @@ export class Loreline {
203
252
  *
204
253
  * @param input The Loreline script content as a string (`.lor` format)
205
254
  * @param filePath Optional file path of the input being parsed. If provided, requires `handleFile` as well.
206
- * @param handleFile Optional file handler to read imports. If that handler is asynchronous, then `parse()` will return null and `callback` argument should be used
255
+ * @param handleFile Optional file handler used to read imports. If it is asynchronous, `parse()` returns null and you should use the `callback` argument instead.
207
256
  * @param callback If provided, will be called with the resulting script as argument. Mostly useful when reading file imports asynchronously
208
257
  * @returns The parsed script as an AST `Script` instance (if loaded synchronously)
209
258
  * @throws If the script contains syntax errors or other parsing issues
@@ -219,7 +268,7 @@ export class Loreline {
219
268
  *
220
269
  * @param script The parsed script (result from `parse()`)
221
270
  * @param handleDialogue Function called when dialogue text should be displayed
222
- * @param handleChoice Function called when player needs to make a choice
271
+ * @param handleChoice Function called when the player needs to make a choice
223
272
  * @param handleFinish Function called when script execution completes
224
273
  * @param beatName Optional name of a specific beat to start from (defaults to first beat)
225
274
  * @param options Additional options
@@ -235,18 +284,18 @@ export class Loreline {
235
284
  ): Interpreter;
236
285
 
237
286
  /**
238
- * Resumes a previously saved Loreline script from its saved state.
287
+ * Resumes a Loreline script from a saved state.
239
288
  *
240
289
  * This allows you to continue a story from the exact point where it was saved,
241
290
  * restoring all state variables, choices, and player progress.
242
291
  *
243
292
  * @param script The parsed script (result from `parse()`)
244
293
  * @param handleDialogue Function called when dialogue text should be displayed
245
- * @param handleChoice Function called when player needs to make a choice
294
+ * @param handleChoice Function called when the player needs to make a choice
246
295
  * @param handleFinish Function called when script execution completes
247
296
  * @param saveData The saved game data (typically from `interpreter.save()`)
248
297
  * @param beatName Optional beat name to override where to resume from
249
- * @param options Optional options to configure interpreter behavior
298
+ * @param options Optional interpreter options
250
299
  * @returns The interpreter instance that is running the script
251
300
  */
252
301
  static resume(
@@ -260,13 +309,13 @@ export class Loreline {
260
309
  ): Interpreter;
261
310
 
262
311
  /**
263
- * Extracts translations from a parsed translation script.
312
+ * Extracts the translations of a parsed translation file, by their `#id`.
264
313
  *
265
- * Given a translation file parsed with `parse()`, this returns a translations map
266
- * that can be passed as `options.translations` to `play()` or `resume()`.
314
+ * To play a story in a language, use `loadLocale()` instead: the interpreter
315
+ * looks translations up by file, which this map doesn't hold.
267
316
  *
268
317
  * @param script The parsed translation script (result from `parse()` on a `.XX.lor` file)
269
- * @returns A translations map to pass as `InterpreterOptions.translations`
318
+ * @returns The translations of the file, by `#id`
270
319
  */
271
320
  static extractTranslations(script: Script): Translations;
272
321
 
@@ -298,7 +347,7 @@ export class Loreline {
298
347
  ): Translations | null;
299
348
 
300
349
  /**
301
- * Enable or disable runtime support for an alternate translation file format.
350
+ * Enables or disables runtime support for an alternative translation file format.
302
351
  *
303
352
  * By default only `.<locale>.lor` files are tried by `loadLocale`. Call
304
353
  * this to opt in to additional formats:
@@ -319,13 +368,23 @@ export class Loreline {
319
368
  *
320
369
  * In async mode (callback supplied) the callback fires with `null` on
321
370
  * failure and this method tells you what went wrong. In sync mode the
322
- * call throws, and this is set to the same error so it can be inspected
323
- * after the catch.
371
+ * call throws, and this method returns the same error so it can be
372
+ * inspected after the catch.
324
373
  *
325
374
  * Not thread-safe: read immediately after the call returns.
326
375
  */
327
376
  static lastError(): Error | null;
328
377
 
378
+ /**
379
+ * The names of the functions a script declares without a body, imports
380
+ * included. The app running the story must provide them in the `functions`
381
+ * option: calling one it doesn't provide is an error. A tool that plays
382
+ * scripts on their own, such as a preview, can provide stand-ins for these.
383
+ *
384
+ * @param script The parsed script (result from `parse()`)
385
+ */
386
+ static externalFunctions(script: Script): string[];
387
+
329
388
  /**
330
389
  * Prints a parsed script back into Loreline source code.
331
390
  *
@@ -376,9 +435,11 @@ export class Interpreter {
376
435
  /**
377
436
  * Restores the interpreter state from a SaveData object.
378
437
  * This allows resuming execution from a previously saved state.
438
+ * Live child interpreters are disposed, and saved children can then be
439
+ * continued with `resumeSpawn()`.
379
440
  *
380
441
  * @param saveData The SaveData object containing the serialized state
381
- * @throws RuntimeError If the save data version is incompatible
442
+ * @throws RuntimeError If the save data version is incompatible, or if called on a child interpreter
382
443
  */
383
444
  restore(saveData: SaveData): void;
384
445
 
@@ -388,6 +449,83 @@ export class Interpreter {
388
449
  */
389
450
  resume(): void;
390
451
 
452
+ /**
453
+ * Key of this interpreter when it was spawned from another one, null for a root interpreter.
454
+ */
455
+ readonly key: string | null;
456
+
457
+ /**
458
+ * Whether this interpreter is a root interpreter (not spawned from another one).
459
+ */
460
+ isRoot(): boolean;
461
+
462
+ /**
463
+ * Spawns a new child interpreter that shares everything with this one (script, state,
464
+ * characters, functions) except the playhead. The child is not started: call `start()`
465
+ * on it. Any live child using the same key is disposed first, and a restored flow still
466
+ * pending for that key is discarded (use `resumeSpawn()` to continue it instead).
467
+ *
468
+ * Calling `save()` on any interpreter of the family saves the shared state and every playhead.
469
+ *
470
+ * A function stored as a value in shared state stays bound to the interpreter that
471
+ * read it: calling it from another interpreter reads the playhead of the first one.
472
+ * Call functions by name instead of storing them in shared state.
473
+ *
474
+ * @param key Identifies the child, notably to resume it after a restore
475
+ * @param handleDialogue Dialogue handler of the child (defaults to the one of this interpreter)
476
+ * @param handleChoice Choice handler of the child (defaults to the one of this interpreter)
477
+ * @param handleFinish Finish handler of the child (defaults to the one of this interpreter)
478
+ * @returns The child interpreter
479
+ */
480
+ spawn(key: string, handleDialogue?: DialogueHandler | null, handleChoice?: ChoiceHandler | null, handleFinish?: FinishHandler | null): Interpreter;
481
+
482
+ /**
483
+ * Rebuilds a child interpreter from the flow saved under the given key, after `restore()`
484
+ * was called on the root interpreter (or after `Loreline.resume()`). The child is not
485
+ * resumed yet: call `resume()` on it.
486
+ *
487
+ * @param key The key the child had when it was saved (see `resumableSpawnKeys()`)
488
+ * @param handleDialogue Dialogue handler of the child (defaults to the one of this interpreter)
489
+ * @param handleChoice Choice handler of the child (defaults to the one of this interpreter)
490
+ * @param handleFinish Finish handler of the child (defaults to the one of this interpreter)
491
+ * @returns The restored child interpreter
492
+ * @throws RuntimeError If no restored flow is pending for this key
493
+ */
494
+ resumeSpawn(key: string, handleDialogue?: DialogueHandler | null, handleChoice?: ChoiceHandler | null, handleFinish?: FinishHandler | null): Interpreter;
495
+
496
+ /**
497
+ * Keys of the child interpreters read by the last restore that were not
498
+ * resumed with `resumeSpawn()` (nor replaced with `spawn()`) yet.
499
+ */
500
+ resumableSpawnKeys(): Array<string>;
501
+
502
+ /**
503
+ * Reseeds the random generator shared by this interpreter, its root and every child.
504
+ * The generator is part of save data, so a restored game draws the same random values
505
+ * as it would have without the save. Call this after a restore to break that on purpose.
506
+ *
507
+ * @param seed The seed of the new sequence. Without one, a seed is taken from the clock.
508
+ */
509
+ seedRandom(seed?: number | null): void;
510
+
511
+ /**
512
+ * Builds now what the interpreter would otherwise build the first time it needs it,
513
+ * so that the cost is paid at a chosen moment, like a loading screen, rather than
514
+ * during play: for now, what each `when` block needs to pick a rule, which takes a
515
+ * moment on blocks of thousands of rules. Optional, it never changes what the script
516
+ * plays, and a second call only builds what is missing. Interpreters spawned from
517
+ * this one share these caches. See also the `prepareCaches` option.
518
+ */
519
+ prepareCaches(): void;
520
+
521
+ /**
522
+ * Stops a child interpreter for good: its playhead is cleared, it is not part of
523
+ * saves anymore, and callbacks it handed out become no-ops.
524
+ *
525
+ * @throws RuntimeError If called on a root interpreter
526
+ */
527
+ dispose(): void;
528
+
391
529
  /**
392
530
  * Gets a character by name.
393
531
  *
@@ -470,7 +608,7 @@ export class Interpreter {
470
608
  }
471
609
 
472
610
  /**
473
- * An object to store a Int64 from two `high` and `low` number values.
611
+ * An object that stores an Int64 as two numbers, `high` and `low`.
474
612
  */
475
613
  export interface Int64 {
476
614
  /**
@@ -558,6 +696,14 @@ export class Script extends Node {
558
696
  */
559
697
  filePath: string | null;
560
698
 
699
+ /**
700
+ * Gets a field of the story's metadata block, as written in the script.
701
+ *
702
+ * @param name The name of the field to get
703
+ * @return The field value or null if not found
704
+ */
705
+ getMetadataField(name: string): any;
706
+
561
707
  /**
562
708
  * Reconstructs a Script from a JSON representation.
563
709
  * @param json The JSON object (as returned by script.toJson())
@@ -567,8 +713,8 @@ export class Script extends Node {
567
713
  }
568
714
 
569
715
  /**
570
- * Base interface to hold loreline values
571
- * This interface allows to map loreline object fields to game-specific objects.
716
+ * Base interface for objects that hold Loreline values.
717
+ * It lets you map Loreline object fields to game-specific objects.
572
718
  */
573
719
  export interface Fields {
574
720
  /**
@@ -577,12 +723,12 @@ export interface Fields {
577
723
  lorelineCreate(interpreter: Interpreter): void;
578
724
 
579
725
  /**
580
- * Get the value associated to the given field key
726
+ * Gets the value associated with the given field key
581
727
  */
582
728
  lorelineGet(interpreter: Interpreter, key: string): any;
583
729
 
584
730
  /**
585
- * Set the value associated to the given field key
731
+ * Sets the value associated with the given field key
586
732
  */
587
733
  lorelineSet(interpreter: Interpreter, key: string, value: any): void;
588
734