Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ jobs:
- run: npm run lint
# --check builds twice and fails when the two outputs differ
- run: node scripts/build.mjs --check
- run: npm run contract:check
- name: Package contents
run: npm pack --dry-run
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
- run: npm run typecheck
- run: npm run lint
- run: node scripts/build.mjs --check
- run: npm run contract:check

- name: Check the tag against package.json
if: startsWith(github.ref, 'refs/tags/v')
Expand Down
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ npm run typecheck # type-checks tests/types/ against types/
npm run lint # ESLint's recommended rules (eslint.config.js)
npm run build # dist/: ESM, minified ESM and IIFE bundles with source maps
node scripts/build.mjs --check # builds twice and fails if the outputs differ
npm run contract:check # package.json, entry points, types, schemas and docs agree (after a build)
```

The source is plain JavaScript ES modules (`src/`), with type declarations in `types/`. Keep both in step when the
public API changes, and document it in [docs/api.md](docs/api.md).
public API changes, and document it in the entry points table of [docs/api.md](docs/api.md) (or of the entry's own
document); `npm run contract:check` compares the three, and checks the documents' links, the format ids and the
schemas.

### Tests

Expand Down
1 change: 1 addition & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ npm test # unit tests (node --test, synthetic inputs only)
npm run build # browser bundles in dist/
npm run typecheck # checks the type declarations in types/
npm run lint # ESLint (eslint.config.js)
npm run contract:check # package.json, entry exports, types, schemas and docs agree
npm run validate-data -- <site dir> [chart id ...] # validates a site against the data format
node scripts/story-author.mjs help # the commands of the story script tool (experimental)
OURNOTES_DATA=<site dir> npm run test:data # opt-in: runs the player in Node on real chart data
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ npm test # 单元测试(node --test,仅使用合成输入
npm run build # 构建 dist/ 下的浏览器包
npm run typecheck # 检查 types/ 下的类型声明
npm run lint # ESLint 检查(eslint.config.js)
npm run contract:check # 核对 package.json、入口导出、类型声明、schema 与文档是否一致
npm run validate-data -- <站点目录> [谱面 id ...] # 按数据格式校验一个站点
node scripts/story-author.mjs help # 自写剧情工具(实验功能)的命令
OURNOTES_DATA=<站点目录> npm run test:data # 可选:用真实谱面数据在 Node 中运行播放器
Expand Down
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ live UI, with the chart's music and sound effects, auto-played at Perfect. The d
|---|---|
| `ournotes-player` | `ChartPlayer`, `ChartSession`, `AssetStore`, `defineOurnotesPlayer`, `OurnotesPlayerElement`, `LIVE_SPEEDS`, `LIVE_OPTIONS`, `LIVE_OPTION_GROUPS`, `LiveSettingsError`, `PLAYER_LANGUAGES`, `formatTime` |
| `ournotes-player/element` | the same exports; importing it defines `<ournotes-player>` |
| `ournotes-player/ui` (experimental) | `UILibrary`, `UISession`, `UIPlayer`, `OurnotesUIElement`, `defineOurnotesUI`; [prefab preview API and limits](ui-preview.md) |
| `ournotes-player/ui` (experimental) | `UILibrary`, `UISession`, `UIPlayer`, `OurnotesUIElement`, `defineOurnotesUI`, `cameraProjection`; [prefab preview API and limits](ui-preview.md) |
| `ournotes-player/ui/element` (experimental) | the same UI exports; importing it defines `<ournotes-ui>` |
| `ournotes-player/replay/preset` | `parseJustJudgementTypes`, `applyAccuracyPreset`, `applySegmentPreset`: declared input plans that set the judgement grades of an `ournotes.replay/1` request in place |

Expand Down
70 changes: 55 additions & 15 deletions docs/story-author.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ node scripts/story-author.mjs new stories/10002.json $S > story.json # 4. a st
node scripts/story-author.mjs check story.json $S # 5. errors and warnings with JSON pointers
node scripts/story-author.mjs render story.json $S --out shots # 6. one picture per line, and a contact sheet
# look at shots/sheet.png (and shots/line-*.png), fix, repeat 5-6
# (--fix <file> on check and render writes the script with the fixes the tool knows applied)
node scripts/story-author.mjs build story.json $S --name my-story # 7. into the site
node scripts/story-author.mjs serve $S # 8. URLs to watch it in examples/story
```
Expand All @@ -42,8 +43,10 @@ Look at the pictures after each render: the contact sheet shows every line with

## Commands

All commands take `--site <dir>`. Exit status: 0 done, 1 the script has errors or the job failed (the JSON has
`"ok": false` and the errors, or `error`), 2 a usage error (message on stderr).
All commands take `--site <dir>`. The commands that draw pictures take `--chrome`, `--gpu` and `--core` (see
[Requirements](#requirements)) and give up a picture job that has not finished after `--timeout <s>` seconds (default
600). Exit status: 0 done, 1 the script has errors or the job failed (the JSON has `"ok": false` and the errors, or
`error`), 2 a usage error (message on stderr).

### bases

Expand Down Expand Up @@ -98,15 +101,16 @@ stage, three lines). Redirect it to a file and edit it.

### check

`check <script>`: the script's errors and warnings. A script without errors also reports its number of lines and
scenes and the CSS font family its text is drawn with.
`check <script> [--fix <file>]`: the script's errors and warnings. A script without errors also reports its number of
lines and scenes and the CSS font family its text is drawn with.

```json
{
"ok": false, "language": "zh-Hans",
"errors": [{ "path": "/beats/2/motion", "code": "unknownMotion",
"message": "motion \"mtn_nodd01\" does not exist in adv_live2d_…; did you mean \"mtn_nod01\"?",
"expected": ["mtn_angry01", "…"], "suggestion": "mtn_nod01" }],
"expected": ["mtn_angry01", "…"], "suggestion": "mtn_nod01",
"fix": [{ "op": "replace", "path": "/beats/2/motion", "value": "mtn_nod01" }] }],
"warnings": [{ "path": "/beats/0/say", "code": "speakerOffStage", "message": "tomori speaks from off stage" }]
}
```
Expand All @@ -115,31 +119,65 @@ Every problem has `path`, a JSON pointer to the value in the script, `code` and
comes with `expected` (the names that do, or `expectedCount` when there are more than 40) and, when one is close,
`suggestion`.

### Fixes

A problem whose fix is known has `fix`: [JSON Patch](https://www.rfc-editor.org/rfc/rfc6902) operations on the script
document (`replace`, `add`, and `move` within one object to rename a key, which keeps the key's place). The fixes:

| Code | Fix |
|---|---|
| `unknownKey` | The key renamed to the nearest key of the format, when the object does not have that key already. |
| `unknownLanguage`, `unknownFont`, `unknownPosition`, `unknownCamera`, `unknownModel`, `unknownStage`, `unknownMusic`, `unknownMotion`, `unknownExpression` | The nearest name (`suggestion`) written in. |
| `unknownCast` | The nearest cast id written in; in `positions`, the key renamed, when the beat does not place that cast member already. |
| `duplicateId` | The id with the first free `_<n>` after it (`promise_2`). |
| `positionTaken` | The free position nearest the taken one (`suggestion`), the left one of two as near. |
| `textTooWide`, `textTooTall` | `render`: the text broken where the game's text engine wraps it in the talk window, and, where the window holds fewer lines, spread over beats after it (the same speaker, or narration), as evenly as fits. `suggestion` holds the texts. A line with both warnings has the fix on the first. |

`--fix <file>` on `check` and `render` writes the script with every fix applied to `<file>` (it may be the script
itself; the fixes of later beats go in first, so the beats a fix adds do not move the others) and checks it again:

```json
"fixed": { "file": "story.fixed.json", "applied": [{ "path": "/beats/2/motion", "code": "unknownMotion" }],
"failed": [], "ok": true, "errors": 0, "warnings": 1 }
```

`applied` and `failed` (with `message`) list the problems whose fixes went in or could not; `ok`, `errors` and
`warnings` are how the written script checks (`render` does not render it again). A problem without `fix` needs the
author: it has no near name, or its fix is a choice of story (who stands where, who speaks).

### render

`render <script> --out <dir> [--from <line>] [--to <line>] [--width <px>] [--settle <frames>] [--columns <n>]`: plays
the script in the story player (manual advance, no sound) and saves a picture of each line from `--from` to `--to`
(line numbers from 0; default every line) once its text is fully shown and `--settle` more frames have passed
(default 8, at 30 frames per second): `line-<n>.png` at `--width` × width × 6 / 13 pixels (default 1300 × 600), and
`sheet.png` (`sheet-<n>.png` for more than 12 lines) with every line, its speaker and its text. Earlier pictures of a
render in `--out` are replaced. A start line after 0 is reached the way the game's shortcut reaches it: the rows before
it run without being shown.
`render <script> --out <dir> [--from <line>] [--to <line>] [--width <px>] [--settle <frames>] [--columns <n>]
[--fix <file>]`: plays the script in the story player (manual advance, no sound) and saves a picture of each line from
`--from` to `--to` (line numbers from 0; default every line) once its text is fully shown and `--settle` more frames
have passed (default 8, at 30 frames per second): `line-<n>.png` at `--width` × width × 6 / 13 pixels (default 1300 ×
600), and `sheet.png` (`sheet-<n>.png` for more than 12 lines) with every line, its speaker and its text. Earlier
pictures of a render in `--out` are replaced. A start line after 0 is reached the way the game's shortcut reaches it:
the rows before it run without being shown.

```json
{
"ok": true, "out": "shots", "sheets": ["shots/sheet.png"],
"frames": [{ "line": 0, "path": "/beats/1", "speaker": "soyo", "text": "…", "image": "shots/line-000.png",
"talk": { "lines": 1, "fontSize": 36, "width": 431.2, "widthLimit": 1200, "height": 41.6,
"heightLimit": 200, "overflow": false } }],
"warnings": [{ "path": "/beats/7", "code": "textTooWide", "message": "line 4 runs past the talk window …" }],
"heightLimit": 200, "overflow": false },
"timing": { "characters": 11, "typing": 1.1, "autoHold": 1.6 } }],
"warnings": [{ "path": "/beats/7", "code": "textTooWide", "message": "line 4 runs past the talk window …",
"suggestion": ["……", "……"], "fix": [{ "op": "replace", "path": "/beats/7/text", "value": "……" }] }],
"seconds": 7.7
}
```

`talk` is the layout of the line's text in the talk window, in the window's units: the number of text lines, the
point size, the widest line and the height of the text against the room the window has. The talk window does not
wrap text: a line wider than the window runs past its edge (warning `textTooWide`; break it with `\n`), and more text
lines than fit run below it (`textTooTall`; split it into two lines of the script).
lines than fit run below it (`textTooTall`; split it into two lines of the script). Both come with a fix (see
[Fixes](#fixes)) when breaking the text at the game's word wrap makes it fit.

`timing` is the line's pacing in seconds of story time: `characters`, the characters the typewriter types; `typing`,
from the line's start until its text is fully shown (the game's ADV frame rate, 30 frames per second); `autoHold`, how
long auto mode then keeps the line before the next one (`_waitTalkTextUnitTime` per character, at least
`_minTalkDisplayTime`, from the base story's player settings). A line takes `typing` + `autoHold` in auto mode.

### build

Expand Down Expand Up @@ -233,6 +271,8 @@ breaks the line. Keep each text line within the talk window's width; `render` re
| warning `unusedCast` | A cast member never appears or speaks. |
| warning `textTooWide`, `textTooTall` | `render`: a line does not fit the talk window. |

Codes with a known fix are listed under [Fixes](#fixes).

## What the story is made of

The tool compiles a script into the story player's data ([story-data-format.md](story-data-format.md)): an
Expand Down
2 changes: 1 addition & 1 deletion docs/story.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ mouth from the CRI Lips analysis in the story's own data ([crilips.md](crilips.m

| Import | Contents |
|---|---|
| `ournotes-player/story` | `StoryPlayer`, `StorySession`, `AssetStore`, `loadStoryStore`, `fetchStoryManifest`, `defineOurnotesStory`, `OurnotesStoryElement`, `registerCommand`, `registeredCommands`, `STORY_FRAME_RATE` (30), `STORY_MANIFEST_FORMAT` / `STORY_MANIFEST_FORMATS` (the current story manifest format and the ones the player reads), the game's enums (`ADV_COMMAND`, `ADV_PLAYBACK_SPEED`, `ADV_PLAYBACK_MODE`, `ADV_CANVAS_LAYER`) |
| `ournotes-player/story` | `StoryPlayer`, `StorySession`, `SimpleStorySession` (the session of an Overlay episode), `AssetStore`, `loadStoryStore`, `fetchStoryManifest`, `defineOurnotesStory`, `OurnotesStoryElement`, `parseStorySpeed`; the command registry (`registerCommand`, `registeredCommands`, `unregisteredCommands`, `StoryPlayerCore`, `StoryCommandError`) and the story UI interface (`checkStoryUI`, `StoryUILayer`, `createStoryUILayers`); `STORY_FRAME_RATE` (30), `STORY_MANIFEST_FORMAT` / `STORY_MANIFEST_FORMATS` (the current story manifest format and the ones the player reads), `STORY_LANGUAGES` (language code → text field), `storyLines` (the rows that show a line), `advViewport` (the 13:6 ADV viewport in a screen), the control labels (`STORY_STRINGS`, `storyStrings`), the game's enums (`ADV_COMMAND`, `ADV_PLAYBACK_SPEED`, `ADV_PLAYBACK_SPEEDS`, `ADV_PLAYBACK_MODE`, `ADV_CANVAS_LAYER`) |
| `ournotes-player/story/element` | the same exports; importing it defines `<ournotes-story>` |

Browser bundles in `dist/`: `ournotes-player.story.js` (ESM, the API), `ournotes-player.story.element.js` (ESM, defines
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"test:data": "node --test tests/data/*.test.mjs",
"typecheck": "tsc -p tests/types",
"lint": "eslint .",
"contract:check": "node scripts/check-contracts.mjs",
"validate-data": "node scripts/validate-data.mjs",
"read-set": "node scripts/read-set.mjs"
},
Expand Down
Loading
Loading